summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj12
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ButtonCellContentView.swift3
-rw-r--r--ios/MullvadVPN/Extensions/CGSize+Helpers.swift18
-rw-r--r--ios/MullvadVPN/Extensions/UIImage+Helpers.swift29
-rw-r--r--ios/MullvadVPN/UI appearance/NSDirectionalEdgeInsets+Helpers.swift21
-rw-r--r--ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift26
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/DisconnectSplitButton.swift78
-rw-r--r--ios/MullvadVPN/Views/AppButton.swift69
-rw-r--r--ios/MullvadVPN/Views/CustomButton.swift154
-rw-r--r--ios/MullvadVPN/Views/InAppPurchaseButton.swift19
10 files changed, 146 insertions, 283 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 2aa72ee4fb..85fb900632 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -449,7 +449,6 @@
58FF9FE42B075BDD00E4C97D /* EditAccessMethodItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF9FE32B075BDD00E4C97D /* EditAccessMethodItemIdentifier.swift */; };
58FF9FE82B07650A00E4C97D /* ButtonCellContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF9FE72B07650A00E4C97D /* ButtonCellContentConfiguration.swift */; };
58FF9FEA2B07653800E4C97D /* ButtonCellContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF9FE92B07653800E4C97D /* ButtonCellContentView.swift */; };
- 58FF9FEC2B07A7CB00E4C97D /* NSDirectionalEdgeInsets+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF9FEB2B07A7CB00E4C97D /* NSDirectionalEdgeInsets+Helpers.swift */; };
58FF9FF02B07C4D300E4C97D /* PersistentAccessMethod+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF9FEF2B07C4D300E4C97D /* PersistentAccessMethod+ViewModel.swift */; };
58FF9FF42B07C61B00E4C97D /* AccessMethodValidationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF9FF32B07C61B00E4C97D /* AccessMethodValidationError.swift */; };
7A02D4EB2A9CEC7A00C19E31 /* MullvadVPNScreenshots.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = 7A02D4EA2A9CEC7A00C19E31 /* MullvadVPNScreenshots.xctestplan */; };
@@ -896,6 +895,8 @@
F06045E62B231EB700B2D37A /* URLSessionTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = F06045E52B231EB700B2D37A /* URLSessionTransport.swift */; };
F06045EA2B23217E00B2D37A /* ShadowsocksTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = F06045E92B23217E00B2D37A /* ShadowsocksTransport.swift */; };
F06045EC2B2322A500B2D37A /* Jittered.swift in Sources */ = {isa = PBXBuildFile; fileRef = F06045EB2B2322A500B2D37A /* Jittered.swift */; };
+ F062000A2CB7EB42002E6DB9 /* CGSize+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = F06200092CB7EB42002E6DB9 /* CGSize+Helpers.swift */; };
+ F062000C2CB7EB5D002E6DB9 /* UIImage+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = F062000B2CB7EB5D002E6DB9 /* UIImage+Helpers.swift */; };
F062B94D2C16E09700B6D47A /* TunnelSettingsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F062B94C2C16E09700B6D47A /* TunnelSettingsManagerTests.swift */; };
F072D3CF2C07122400906F64 /* SettingsUpdaterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F072D3CE2C07122400906F64 /* SettingsUpdaterTests.swift */; };
F072D3D22C071AD100906F64 /* ShadowsocksLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F072D3D12C071AD100906F64 /* ShadowsocksLoaderTests.swift */; };
@@ -1785,7 +1786,6 @@
58FF9FE32B075BDD00E4C97D /* EditAccessMethodItemIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessMethodItemIdentifier.swift; sourceTree = "<group>"; };
58FF9FE72B07650A00E4C97D /* ButtonCellContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonCellContentConfiguration.swift; sourceTree = "<group>"; };
58FF9FE92B07653800E4C97D /* ButtonCellContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonCellContentView.swift; sourceTree = "<group>"; };
- 58FF9FEB2B07A7CB00E4C97D /* NSDirectionalEdgeInsets+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSDirectionalEdgeInsets+Helpers.swift"; sourceTree = "<group>"; };
58FF9FEF2B07C4D300E4C97D /* PersistentAccessMethod+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistentAccessMethod+ViewModel.swift"; sourceTree = "<group>"; };
58FF9FF32B07C61B00E4C97D /* AccessMethodValidationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodValidationError.swift; sourceTree = "<group>"; };
7A02D4EA2A9CEC7A00C19E31 /* MullvadVPNScreenshots.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = MullvadVPNScreenshots.xctestplan; sourceTree = "<group>"; };
@@ -2117,6 +2117,8 @@
F06045E52B231EB700B2D37A /* URLSessionTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTransport.swift; sourceTree = "<group>"; };
F06045E92B23217E00B2D37A /* ShadowsocksTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksTransport.swift; sourceTree = "<group>"; };
F06045EB2B2322A500B2D37A /* Jittered.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Jittered.swift; sourceTree = "<group>"; };
+ F06200092CB7EB42002E6DB9 /* CGSize+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGSize+Helpers.swift"; sourceTree = "<group>"; };
+ F062000B2CB7EB5D002E6DB9 /* UIImage+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Helpers.swift"; sourceTree = "<group>"; };
F062B94C2C16E09700B6D47A /* TunnelSettingsManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsManagerTests.swift; sourceTree = "<group>"; };
F072D3CE2C07122400906F64 /* SettingsUpdaterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsUpdaterTests.swift; sourceTree = "<group>"; };
F072D3D12C071AD100906F64 /* ShadowsocksLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksLoaderTests.swift; sourceTree = "<group>"; };
@@ -2960,6 +2962,7 @@
isa = PBXGroup;
children = (
5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */,
+ F06200092CB7EB42002E6DB9 /* CGSize+Helpers.swift */,
587EB669270EFACB00123C75 /* CharacterSet+IPAddress.swift */,
58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift */,
7AF9BE8F2A39F26000DBFEDB /* Collection+Sorting.swift */,
@@ -2981,6 +2984,7 @@
5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */,
587CBFE222807F530028DED3 /* UIColor+Helpers.swift */,
7ABE318C2A1CDD4500DF4963 /* UIFont+Weight.swift */,
+ F062000B2CB7EB5D002E6DB9 /* UIImage+Helpers.swift */,
58CEB2FA2AFD13E600E6E088 /* UIListContentConfiguration+Extensions.swift */,
58CEB2FC2AFD19D300E6E088 /* UITableView+ReuseIdentifier.swift */,
7A58699A2B482FE200640D27 /* UITableViewCell+Disable.swift */,
@@ -3016,7 +3020,6 @@
children = (
58CCA0152242560B004F3011 /* UIColor+Palette.swift */,
A9E034632ABB302000E59A5A /* UIEdgeInsets+Extensions.swift */,
- 58FF9FEB2B07A7CB00E4C97D /* NSDirectionalEdgeInsets+Helpers.swift */,
585CA70E25F8C44600B47C62 /* UIMetrics.swift */,
);
path = "UI appearance";
@@ -5631,6 +5634,7 @@
587EB672271451E300123C75 /* VPNSettingsViewModel.swift in Sources */,
586A950C290125EE007BAF2B /* AlertPresenter.swift in Sources */,
7A9FA1422A2E3306000B728D /* CheckboxView.swift in Sources */,
+ F062000A2CB7EB42002E6DB9 /* CGSize+Helpers.swift in Sources */,
586C0D892B03D5E000E7CDD7 /* TextCellContentConfiguration+Extensions.swift in Sources */,
58C3F4F92964B08300D72515 /* MapViewController.swift in Sources */,
584D26C6270C8741004EA533 /* SettingsDNSTextCell.swift in Sources */,
@@ -5644,6 +5648,7 @@
7A516C2E2B6D357500BBD33D /* URL+Scoping.swift in Sources */,
5878A27529093A310096FC88 /* StorePaymentEvent.swift in Sources */,
7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */,
+ F062000C2CB7EB5D002E6DB9 /* UIImage+Helpers.swift in Sources */,
7A6389EB2B7FAD7A008E77E1 /* SettingsFieldValidationErrorContentView.swift in Sources */,
58B26E2A2943545A00D5980C /* NotificationManagerDelegate.swift in Sources */,
58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */,
@@ -5840,7 +5845,6 @@
7A5869B92B56E7F000640D27 /* IPOverrideViewControllerDelegate.swift in Sources */,
586C0D7A2B039CE300E7CDD7 /* ShadowsocksCipherPicker.swift in Sources */,
58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */,
- 58FF9FEC2B07A7CB00E4C97D /* NSDirectionalEdgeInsets+Helpers.swift in Sources */,
586C0D832B03D2FF00E7CDD7 /* ShadowsocksSectionHandler.swift in Sources */,
58B26E262943522400D5980C /* NotificationProvider.swift in Sources */,
58CE5E64224146200008646E /* AppDelegate.swift in Sources */,
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ButtonCellContentView.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ButtonCellContentView.swift
index c002057ac5..e294716ad4 100644
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ButtonCellContentView.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ButtonCellContentView.swift
@@ -66,8 +66,7 @@ class ButtonCellContentView: UIView, UIContentView {
button.titleLabel?.font = .systemFont(ofSize: 17)
button.isEnabled = actualConfiguration.isEnabled
button.style = actualConfiguration.style
- button.overrideContentEdgeInsets = true
- button.directionalContentEdgeInsets = actualConfiguration.directionalContentEdgeInsets
+ button.configuration?.contentInsets = actualConfiguration.directionalContentEdgeInsets
}
private func addSubviews() {
diff --git a/ios/MullvadVPN/Extensions/CGSize+Helpers.swift b/ios/MullvadVPN/Extensions/CGSize+Helpers.swift
new file mode 100644
index 0000000000..a6a7956c5f
--- /dev/null
+++ b/ios/MullvadVPN/Extensions/CGSize+Helpers.swift
@@ -0,0 +1,18 @@
+//
+// CGSize+Helpers.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2024-10-10.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+extension CGSize {
+ // Function to deduct insets from CGSize
+ func deducting(insets: NSDirectionalEdgeInsets) -> CGSize {
+ let newWidth = width - (insets.leading + insets.trailing)
+ let newHeight = height - (insets.top + insets.bottom)
+ return CGSize(width: newWidth, height: newHeight)
+ }
+}
diff --git a/ios/MullvadVPN/Extensions/UIImage+Helpers.swift b/ios/MullvadVPN/Extensions/UIImage+Helpers.swift
new file mode 100644
index 0000000000..ebf11ea270
--- /dev/null
+++ b/ios/MullvadVPN/Extensions/UIImage+Helpers.swift
@@ -0,0 +1,29 @@
+//
+// UIImage+Helpers.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2024-10-10.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+extension UIImage {
+ // Function to resize image while keeping aspect ratio
+ func resizeImage(targetSize: CGSize) -> UIImage {
+ let widthRatio = targetSize.width / size.width
+ let heightRatio = targetSize.height / size.height
+ let scaleFactor = min(widthRatio, heightRatio)
+
+ // Calculate new size based on the scale factor
+ let newSize = CGSize(width: size.width * scaleFactor, height: size.height * scaleFactor)
+ let renderer = UIGraphicsImageRenderer(size: newSize)
+
+ // Render the new image
+ let resizedImage = renderer.image { _ in
+ draw(in: CGRect(origin: .zero, size: newSize))
+ }
+
+ return resizedImage.withRenderingMode(renderingMode)
+ }
+}
diff --git a/ios/MullvadVPN/UI appearance/NSDirectionalEdgeInsets+Helpers.swift b/ios/MullvadVPN/UI appearance/NSDirectionalEdgeInsets+Helpers.swift
deleted file mode 100644
index e5bda17eb3..0000000000
--- a/ios/MullvadVPN/UI appearance/NSDirectionalEdgeInsets+Helpers.swift
+++ /dev/null
@@ -1,21 +0,0 @@
-//
-// NSDirectionalEdgeInsets+Helpers.swift
-// MullvadVPN
-//
-// Created by pronebird on 17/11/2023.
-// Copyright © 2023 Mullvad VPN AB. All rights reserved.
-//
-
-import UIKit
-
-extension NSDirectionalEdgeInsets {
- /// Converts directional edge insets to `UIEdgeInsets` based on interface direction.
- func toEdgeInsets(_ interfaceDirection: UIUserInterfaceLayoutDirection) -> UIEdgeInsets {
- UIEdgeInsets(
- top: top,
- left: interfaceDirection == .rightToLeft ? trailing : leading,
- bottom: bottom,
- right: interfaceDirection == .rightToLeft ? leading : trailing
- )
- }
-}
diff --git a/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift b/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift
index c021286c0d..e592e1b216 100644
--- a/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift
+++ b/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift
@@ -92,13 +92,9 @@ final class AccountInputGroupView: UIView {
}()
private let lastUsedAccountButton: UIButton = {
- let button = UIButton(type: .system)
+ let button = UIButton(configuration: .plain())
button.translatesAutoresizingMaskIntoConstraints = false
- button.titleLabel?.font = accountNumberFont()
- button.setTitle(" ", for: .normal)
button.contentHorizontalAlignment = .leading
- button.contentEdgeInsets = UIMetrics.textFieldMargins
- button.setTitleColor(UIColor.AccountTextField.NormalState.textColor, for: .normal)
button.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
button.accessibilityLabel = NSLocalizedString(
"LAST_USED_ACCOUNT_ACCESSIBILITY_LABEL",
@@ -106,14 +102,22 @@ final class AccountInputGroupView: UIView {
value: "Last used account",
comment: ""
)
+ button.configuration?.contentInsets = UIMetrics.textFieldMargins.toDirectionalInsets
+ button.configuration?.title = " "
+ button.configuration?
+ .titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { attributeContainer in
+ var updatedAttributeContainer = attributeContainer
+ updatedAttributeContainer.font = AccountInputGroupView.accountNumberFont()
+ updatedAttributeContainer.foregroundColor = .AccountTextField.NormalState.textColor
+ return updatedAttributeContainer
+ }
+
return button
}()
- private let removeLastUsedAccountButton: UIButton = {
- let button = UIButton(type: .custom)
+ private let removeLastUsedAccountButton: CustomButton = {
+ let button = CustomButton()
button.translatesAutoresizingMaskIntoConstraints = false
- button.setImage(UIImage(named: "IconCloseSml"), for: .normal)
- button.imageView?.tintColor = .primaryColor.withAlphaComponent(0.4)
button.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
button.accessibilityLabel = NSLocalizedString(
"REMOVE_LAST_USED_ACCOUNT_ACCESSIBILITY_LABEL",
@@ -121,6 +125,8 @@ final class AccountInputGroupView: UIView {
value: "Remove last used account",
comment: ""
)
+ button.configuration?.image = UIImage(resource: .iconCloseSml).withTintColor(.primaryColor)
+ button.configuration?.title = " "
return button
}()
@@ -303,7 +309,7 @@ final class AccountInputGroupView: UIView {
)
UIView.performWithoutAnimation {
- self.lastUsedAccountButton.setTitle(formattedNumber, for: .normal)
+ self.lastUsedAccountButton.configuration?.title = formattedNumber
self.lastUsedAccountButton.layoutIfNeeded()
}
}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/DisconnectSplitButton.swift b/ios/MullvadVPN/View controllers/Tunnel/DisconnectSplitButton.swift
index 0fcdf7efd1..58bb6819ba 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/DisconnectSplitButton.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/DisconnectSplitButton.swift
@@ -10,80 +10,44 @@ import Foundation
import UIKit
class DisconnectSplitButton: UIView {
- private var secondaryButtonSize: CGSize {
- UIMetrics.DisconnectSplitButton.secondaryButton
- }
-
let primaryButton = AppButton(style: .translucentDangerSplitLeft)
let secondaryButton = AppButton(style: .translucentDangerSplitRight)
- private let secondaryButtonWidthConstraint: NSLayoutConstraint
- private let secondaryButtonHeightConstraint: NSLayoutConstraint
+ override init(frame: CGRect) {
+ super.init(frame: .zero)
+ commonInit()
+ }
- private let stackView: UIStackView
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
- init() {
+ private func commonInit() {
let primaryButtonBlurView = TranslucentButtonBlurView(button: primaryButton)
let secondaryButtonBlurView = TranslucentButtonBlurView(button: secondaryButton)
- stackView = UIStackView(arrangedSubviews: [primaryButtonBlurView, secondaryButtonBlurView])
+ let stackView = UIStackView(arrangedSubviews: [primaryButtonBlurView, secondaryButtonBlurView])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
stackView.distribution = .fill
stackView.alignment = .fill
stackView.spacing = 1
- secondaryButton.setImage(
- UIImage(named: "IconReload")?.imageFlippedForRightToLeftLayoutDirection(),
- for: .normal
- )
-
- primaryButton.overrideContentEdgeInsets = true
- secondaryButtonWidthConstraint = secondaryButton.widthAnchor.constraint(equalToConstant: 0)
- secondaryButtonHeightConstraint = secondaryButton.heightAnchor
- .constraint(equalToConstant: 0)
-
- super.init(frame: .zero)
-
- addSubview(stackView)
-
- NSLayoutConstraint.activate([
- stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
- stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
- stackView.topAnchor.constraint(equalTo: topAnchor),
- stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
-
- secondaryButtonWidthConstraint,
- secondaryButtonHeightConstraint,
- ])
-
- updateTraitConstraints()
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- private func updateTraitConstraints() {
- let newSize = secondaryButtonSize
- secondaryButtonWidthConstraint.constant = newSize.width
- secondaryButtonHeightConstraint.constant = newSize.height
- adjustTitleLabelPosition()
- }
-
- private func adjustTitleLabelPosition() {
- var contentInsets = primaryButton.defaultContentInsets
+ let secondaryButtonSize = UIMetrics.DisconnectSplitButton.secondaryButton
- let offset = stackView.spacing + secondaryButtonSize.width
+ addConstrainedSubviews([stackView]) {
+ stackView.pinEdgesToSuperview()
- if case .leftToRight = effectiveUserInterfaceLayoutDirection {
- contentInsets.left = offset
- contentInsets.right = 0
- } else {
- contentInsets.left = 0
- contentInsets.right = offset
+ secondaryButton.widthAnchor.constraint(equalToConstant: secondaryButtonSize.width)
+ secondaryButton.heightAnchor.constraint(equalToConstant: secondaryButtonSize.height)
}
- primaryButton.contentEdgeInsets = contentInsets
+ // Ideally, we shouldn't need to manually resize the image ourselves.
+ // However, since UIButton.Configuration doesn't provide a direct property
+ // for controlling image scaling (like imageScaling or contentMode in other contexts),
+ // manual resizing has been one approach to ensure the image fits within bounds.
+ secondaryButton.configuration?.image = UIImage(resource: .iconReload)
+ .resizeImage(targetSize: secondaryButtonSize.deducting(insets: secondaryButton.defaultContentInsets))
+ .imageFlippedForRightToLeftLayoutDirection()
}
}
diff --git a/ios/MullvadVPN/Views/AppButton.swift b/ios/MullvadVPN/Views/AppButton.swift
index 3209828d12..0ce8de9a47 100644
--- a/ios/MullvadVPN/Views/AppButton.swift
+++ b/ios/MullvadVPN/Views/AppButton.swift
@@ -11,12 +11,12 @@ import UIKit
/// A subclass that implements action buttons used across the app
class AppButton: CustomButton {
/// Default content insets based on current trait collection.
- var defaultContentInsets: UIEdgeInsets {
+ var defaultContentInsets: NSDirectionalEdgeInsets {
switch traitCollection.userInterfaceIdiom {
case .phone:
- return UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
+ return NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
case .pad:
- return UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 15)
+ return NSDirectionalEdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15)
default:
return .zero
}
@@ -87,30 +87,6 @@ class AppButton: CustomButton {
}
}
- /// Prevents updating `contentEdgeInsets` on changes to trait collection.
- var overrideContentEdgeInsets = false
-
- override var contentEdgeInsets: UIEdgeInsets {
- didSet {
- // Reset inner directional insets when contentEdgeInsets are set directly.
- innerDirectionalContentEdgeInsets = nil
- }
- }
-
- /// Directional content edge insets that are automatically translated into `contentEdgeInsets` immeditely upon updating the property and on trait collection
- /// changes.
- var directionalContentEdgeInsets: NSDirectionalEdgeInsets {
- get {
- innerDirectionalContentEdgeInsets ?? contentEdgeInsets.toDirectionalInsets
- }
- set {
- innerDirectionalContentEdgeInsets = newValue
- updateContentEdgeInsetsFromDirectional()
- }
- }
-
- private var innerDirectionalContentEdgeInsets: NSDirectionalEdgeInsets?
-
init(style: Style) {
self.style = style
super.init(frame: .zero)
@@ -118,7 +94,7 @@ class AppButton: CustomButton {
}
override init(frame: CGRect) {
- style = .default
+ self.style = .default
super.init(frame: frame)
commonInit()
}
@@ -128,32 +104,31 @@ class AppButton: CustomButton {
}
private func commonInit() {
- super.contentEdgeInsets = defaultContentInsets
- imageAlignment = .trailingFixed
+ imageAlignment = .trailing
+ titleAlignment = .leading
- titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .semibold)
-
- let states: [UIControl.State] = [.normal, .highlighted, .disabled]
- states.forEach { state in
- if let titleColor = state.customButtonTitleColor {
- setTitleColor(titleColor, for: state)
+ var config = super.configuration ?? .plain()
+ config.title = title(for: state)
+ config.contentInsets = defaultContentInsets
+ config.background.image = style.backgroundImage
+ config.background.imageContentMode = .scaleAspectFill
+ config.titleTextAttributesTransformer =
+ UIConfigurationTextAttributesTransformer { attributeContainer in
+ var updatedAttributeContainer = attributeContainer
+ updatedAttributeContainer.font = UIFont.systemFont(ofSize: 18, weight: .semibold)
+ return updatedAttributeContainer
}
- }
- // Avoid setting the background image if it's already set via Interface Builder
- if backgroundImage(for: .normal) == nil {
- updateButtonBackground()
+ let configurationHandler: UIButton.ConfigurationUpdateHandler = { button in
+ button.alpha = !button.isEnabled ? 0.5 : 1.0
+ button.configuration?.baseForegroundColor = button.state.customButtonTitleColor
}
+ configuration = config
+ configurationUpdateHandler = configurationHandler
}
/// Set background image based on current style.
private func updateButtonBackground() {
- setBackgroundImage(style.backgroundImage, for: .normal)
- }
-
- /// Update content edge insets from directional edge insets if set.
- private func updateContentEdgeInsetsFromDirectional() {
- guard let directionalEdgeInsets = innerDirectionalContentEdgeInsets else { return }
- super.contentEdgeInsets = directionalEdgeInsets.toEdgeInsets(effectiveUserInterfaceLayoutDirection)
+ configuration?.background.image = style.backgroundImage
}
}
diff --git a/ios/MullvadVPN/Views/CustomButton.swift b/ios/MullvadVPN/Views/CustomButton.swift
index 46353bb1a6..8c99ddb2cc 100644
--- a/ios/MullvadVPN/Views/CustomButton.swift
+++ b/ios/MullvadVPN/Views/CustomButton.swift
@@ -8,39 +8,13 @@
import UIKit
-enum ButtonImageAlignment {
- /// Align image at the left edge of the title label
- case left
-
- /// Align image at the right edge of the title label
- case right
-
- /// Align image at the leading edge of the title label
- case leading
-
- /// Align image at the trailing edge of the title label
- case trailing
-
- /// Align image at the leading edge of content area
- case leadingFixed
-
- /// Align image at the trailing edge of the content area
- case trailingFixed
-
- /// Align image at the left edge of the content area
- case leftFixed
-
- /// Align image at the right edge of the content area
- case rightFixed
-}
-
extension UIControl.State {
var customButtonTitleColor: UIColor? {
switch self {
case .normal:
return UIColor.AppButton.normalTitleColor
case .disabled:
- return UIColor.AppButton.disabledTitleColor.withAlphaComponent(0.5)
+ return UIColor.AppButton.disabledTitleColor
case .highlighted:
return UIColor.AppButton.highlightedTitleColor
default:
@@ -51,53 +25,27 @@ extension UIControl.State {
/// A custom `UIButton` subclass that implements additional layouts for the image
class CustomButton: UIButton {
- var imageAlignment: ButtonImageAlignment = .leading {
+ var imageAlignment: NSDirectionalRectEdge = .leading {
didSet {
- invalidateIntrinsicContentSize()
+ self.configuration?.imagePlacement = imageAlignment
}
}
var inlineImageSpacing: CGFloat = 4 {
didSet {
- invalidateIntrinsicContentSize()
+ self.configuration?.imagePadding = inlineImageSpacing
}
}
- override var intrinsicContentSize: CGSize {
- var intrinsicSize = super.intrinsicContentSize
-
- // Add spacing between the image and title label in intrinsic size calculation
- if let imageSize = currentImage?.size, imageSize.width > 0 {
- intrinsicSize.width += inlineImageSpacing
+ var titleAlignment: UIButton.Configuration.TitleAlignment = .center {
+ didSet {
+ self.configuration?.titleAlignment = titleAlignment
}
-
- return intrinsicSize
}
- var effectiveImageAlignment: ButtonImageAlignment {
- switch (imageAlignment, effectiveUserInterfaceLayoutDirection) {
- case (.left, _),
- (.leading, .leftToRight),
- (.trailing, .rightToLeft):
- return .left
-
- case (.right, _),
- (.trailing, .leftToRight),
- (.leading, .rightToLeft):
- return .right
-
- case (.leftFixed, _),
- (.leadingFixed, .leftToRight),
- (.trailingFixed, .rightToLeft):
- return .leftFixed
-
- case (.rightFixed, _),
- (.trailingFixed, .leftToRight),
- (.leadingFixed, .rightToLeft):
- return .rightFixed
-
- default:
- fatalError()
+ var inlineTitleSpacing: CGFloat = 4 {
+ didSet {
+ self.configuration?.titlePadding = inlineTitleSpacing
}
}
@@ -112,79 +60,13 @@ class CustomButton: UIButton {
}
private func commonInit() {
- // Align the text color with the tint color which is applied to the image view
- if let imageTintColor = UIControl.State.normal.customButtonTitleColor {
- tintColor = imageTintColor
- }
- }
-
- private func computeLayout(forContentRect contentRect: CGRect) -> (CGRect, CGRect) {
- var imageRect = super.imageRect(forContentRect: contentRect)
- var titleRect = super.titleRect(forContentRect: contentRect)
-
- switch (effectiveContentHorizontalAlignment, effectiveImageAlignment) {
- case (.left, .left):
- imageRect.origin.x = contentRect.minX
- titleRect.origin.x = imageRect.width > 0
- ? imageRect.maxX + inlineImageSpacing
- : contentRect.minX
-
- case (.left, .right):
- titleRect.origin.x = contentRect.minX
- imageRect.origin.x = titleRect.maxX + inlineImageSpacing
-
- case (.left, .leftFixed):
- imageRect.origin.x = contentRect.minX
- titleRect.origin.x = imageRect.width > 0
- ? imageRect.maxX + inlineImageSpacing
- : contentRect.minX
-
- case (.left, .rightFixed):
- imageRect.origin.x = contentRect.maxX - imageRect.width
- titleRect.origin.x = contentRect.minX
-
- case (.center, .leftFixed):
- imageRect.origin.x = contentRect.minX
- titleRect.origin.x = contentRect.midX - titleRect.width * 0.5
-
- case (.center, .rightFixed):
- imageRect.origin.x = contentRect.maxX - imageRect.width
- titleRect.origin.x = contentRect.midX - titleRect.width * 0.5
-
- case (.center, .left):
- titleRect.origin.x = contentRect.midX - titleRect.width * 0.5
- imageRect.origin.x = titleRect.minX - inlineImageSpacing - imageRect.width
-
- case (.center, .right):
- titleRect.origin.x = contentRect.midX - titleRect.width * 0.5
- imageRect.origin.x = titleRect.maxX + inlineImageSpacing
-
- case (.right, .left):
- titleRect.origin.x = contentRect.maxX - titleRect.width
- imageRect.origin.x = titleRect.minX - imageRect.width - inlineImageSpacing
-
- case (.right, .leftFixed):
- imageRect.origin.x = contentRect.minX
- titleRect.origin.x = contentRect.maxX - titleRect.width
-
- case (.right, .rightFixed):
- imageRect.origin.x = contentRect.maxX - imageRect.width
- titleRect.origin.x = imageRect.width > 0
- ? imageRect.minX - inlineImageSpacing - titleRect.width
- : contentRect.maxX - titleRect.width
-
- default:
- fatalError()
- }
-
- return (titleRect, imageRect)
- }
-
- override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
- computeLayout(forContentRect: contentRect).1
- }
-
- override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
- computeLayout(forContentRect: contentRect).0
+ var config = UIButton.Configuration.plain()
+ config.imagePadding = inlineImageSpacing
+ config.imagePlacement = imageAlignment
+ config.titleAlignment = titleAlignment
+ config.titleLineBreakMode = .byWordWrapping
+ config.titlePadding = inlineTitleSpacing
+ config.baseForegroundColor = state.customButtonTitleColor
+ self.configuration = config
}
}
diff --git a/ios/MullvadVPN/Views/InAppPurchaseButton.swift b/ios/MullvadVPN/Views/InAppPurchaseButton.swift
index a87f277536..1f3051f38f 100644
--- a/ios/MullvadVPN/Views/InAppPurchaseButton.swift
+++ b/ios/MullvadVPN/Views/InAppPurchaseButton.swift
@@ -43,13 +43,21 @@ class InAppPurchaseButton: AppButton {
override func layoutSubviews() {
super.layoutSubviews()
- activityIndicator.frame = activityIndicatorRect(
- forContentRect: contentRect(forBounds: bounds)
+ // Calculate the content size after insets
+ let contentSize = frame
+ let contentEdgeInsets = configuration?.contentInsets ?? .zero
+ let finalWidth = contentSize.width - (contentEdgeInsets.leading + contentEdgeInsets.trailing)
+ let finalHeight = contentSize.height - (contentEdgeInsets.top + contentEdgeInsets.bottom)
+ let contentRect = CGRect(
+ origin: frame.origin,
+ size: CGSize(width: finalWidth, height: finalHeight)
)
+ self.titleLabel?.frame = getTitleRect(forContentRect: contentRect)
+ self.activityIndicator.frame = activityIndicatorRect(forContentRect: contentRect)
}
- override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
- var titleRect = super.titleRect(forContentRect: contentRect)
+ private func getTitleRect(forContentRect contentRect: CGRect) -> CGRect {
+ var titleRect = titleLabel?.frame ?? .zero
let activityIndicatorRect = activityIndicatorRect(forContentRect: contentRect)
// Adjust the title frame in case if it overlaps the activity indicator
@@ -76,8 +84,7 @@ class InAppPurchaseButton: AppButton {
frame.origin.x = contentRect.minX
}
- frame.origin.y = contentRect.midY - frame.height * 0.5
-
+ frame.origin.y = contentRect.midY
return frame
}
}