From 0339b0bf382d3a27c100d946354e814a8bc287b9 Mon Sep 17 00:00:00 2001 From: mojganii Date: Fri, 11 Oct 2024 15:43:55 +0200 Subject: [PATCH] Refactor buttons to use UIButton.Configuration --- ios/MullvadVPN.xcodeproj/project.pbxproj | 12 +- .../Cells/ButtonCellContentView.swift | 3 +- .../Extensions/CGSize+Helpers.swift | 18 ++ .../Extensions/UIImage+Helpers.swift | 29 ++++ .../NSDirectionalEdgeInsets+Helpers.swift | 21 --- .../Login/AccountInputGroupView.swift | 26 +-- .../Tunnel/DisconnectSplitButton.swift | 78 +++------ ios/MullvadVPN/Views/AppButton.swift | 71 +++----- ios/MullvadVPN/Views/CustomButton.swift | 154 ++---------------- .../Views/InAppPurchaseButton.swift | 19 ++- 10 files changed, 147 insertions(+), 284 deletions(-) create mode 100644 ios/MullvadVPN/Extensions/CGSize+Helpers.swift create mode 100644 ios/MullvadVPN/Extensions/UIImage+Helpers.swift delete mode 100644 ios/MullvadVPN/UI appearance/NSDirectionalEdgeInsets+Helpers.swift diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 2aa72ee4fb53..85fb90063280 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 = ""; }; 58FF9FE72B07650A00E4C97D /* ButtonCellContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonCellContentConfiguration.swift; sourceTree = ""; }; 58FF9FE92B07653800E4C97D /* ButtonCellContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonCellContentView.swift; sourceTree = ""; }; - 58FF9FEB2B07A7CB00E4C97D /* NSDirectionalEdgeInsets+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSDirectionalEdgeInsets+Helpers.swift"; sourceTree = ""; }; 58FF9FEF2B07C4D300E4C97D /* PersistentAccessMethod+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistentAccessMethod+ViewModel.swift"; sourceTree = ""; }; 58FF9FF32B07C61B00E4C97D /* AccessMethodValidationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodValidationError.swift; sourceTree = ""; }; 7A02D4EA2A9CEC7A00C19E31 /* MullvadVPNScreenshots.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = MullvadVPNScreenshots.xctestplan; sourceTree = ""; }; @@ -2117,6 +2117,8 @@ F06045E52B231EB700B2D37A /* URLSessionTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTransport.swift; sourceTree = ""; }; F06045E92B23217E00B2D37A /* ShadowsocksTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksTransport.swift; sourceTree = ""; }; F06045EB2B2322A500B2D37A /* Jittered.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Jittered.swift; sourceTree = ""; }; + F06200092CB7EB42002E6DB9 /* CGSize+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGSize+Helpers.swift"; sourceTree = ""; }; + F062000B2CB7EB5D002E6DB9 /* UIImage+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Helpers.swift"; sourceTree = ""; }; F062B94C2C16E09700B6D47A /* TunnelSettingsManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsManagerTests.swift; sourceTree = ""; }; F072D3CE2C07122400906F64 /* SettingsUpdaterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsUpdaterTests.swift; sourceTree = ""; }; F072D3D12C071AD100906F64 /* ShadowsocksLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksLoaderTests.swift; sourceTree = ""; }; @@ -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 c002057ac568..e294716ad440 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 000000000000..a6a7956c5f83 --- /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 000000000000..ebf11ea270a4 --- /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 e5bda17eb327..000000000000 --- 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 c021286c0dd0..e592e1b216db 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 0fcdf7efd1b9..58bb6819bac4 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 3209828d127f..0ce8de9a47e1 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 - - 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) + imageAlignment = .trailing + titleAlignment = .leading + + 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 46353bb1a607..8c99ddb2cc6c 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 a87f2775364b..1f3051f38f50 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 } }