diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 63022d950777..811b045d4729 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -142,7 +142,7 @@ 58677712290976FB006F721F /* SettingsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58677711290976FB006F721F /* SettingsInteractor.swift */; }; 5867771429097BCD006F721F /* PaymentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5867771329097BCD006F721F /* PaymentState.swift */; }; 5867771629097C5B006F721F /* ProductState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5867771529097C5B006F721F /* ProductState.swift */; }; - 5868585524054096000B8131 /* AppButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5868585424054096000B8131 /* AppButton.swift */; }; + 5868585524054096000B8131 /* CustomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5868585424054096000B8131 /* CustomButton.swift */; }; 58695AA02A4ADA9200328DB3 /* TunnelObfuscationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58695A9F2A4ADA9200328DB3 /* TunnelObfuscationTests.swift */; }; 58695AA72A4B109F00328DB3 /* TunnelObfuscation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5840231F2A406BF5007B27AC /* TunnelObfuscation.framework */; }; 586A0DCB2A20E359006C731C /* MullvadTypes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223D5294C8E5E0029F5F8 /* MullvadTypes.framework */; }; @@ -507,6 +507,8 @@ 7A3FD1B72AD54ABD0042BEA6 /* AnyTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB982A98F4ED00F578F2 /* AnyTransport.swift */; }; 7A3FD1B82AD54AE60042BEA6 /* TimeServerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB9A2A98F58600F578F2 /* TimeServerProxy.swift */; }; 7A42DEC92A05164100B209BE /* SettingsInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */; }; + 7A5869952B32E9C700640D27 /* LinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869942B32E9C700640D27 /* LinkButton.swift */; }; + 7A5869972B32EA4500640D27 /* AppButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869962B32EA4500640D27 /* AppButton.swift */; }; 7A6B4F592AB8412E00123853 /* TunnelMonitorTimings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */; }; 7A6F2FA52AFA3CB2006D0856 /* AccountExpiryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */; }; 7A6F2FA72AFBB9AE006D0856 /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */; }; @@ -1350,7 +1352,7 @@ 58677711290976FB006F721F /* SettingsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInteractor.swift; sourceTree = ""; }; 5867771329097BCD006F721F /* PaymentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentState.swift; sourceTree = ""; }; 5867771529097C5B006F721F /* ProductState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductState.swift; sourceTree = ""; }; - 5868585424054096000B8131 /* AppButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppButton.swift; sourceTree = ""; }; + 5868585424054096000B8131 /* CustomButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomButton.swift; sourceTree = ""; }; 58695A9D2A4ADA9100328DB3 /* TunnelObfuscationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TunnelObfuscationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 58695A9F2A4ADA9200328DB3 /* TunnelObfuscationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObfuscationTests.swift; sourceTree = ""; }; 586A95112901321B007BAF2B /* IPv6Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPv6Endpoint.swift; sourceTree = ""; }; @@ -1646,6 +1648,8 @@ 7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationBlockObserverSupport.swift; sourceTree = ""; }; 7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMessageHandlerTests.swift; sourceTree = ""; }; 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInputCell.swift; sourceTree = ""; }; + 7A5869942B32E9C700640D27 /* LinkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkButton.swift; sourceTree = ""; }; + 7A5869962B32EA4500640D27 /* AppButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppButton.swift; sourceTree = ""; }; 7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorTimings.swift; sourceTree = ""; }; 7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryTests.swift; sourceTree = ""; }; 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiry.swift; sourceTree = ""; }; @@ -2302,8 +2306,9 @@ 583FE01F29C197ED006E85F9 /* Views */ = { isa = PBXGroup; children = ( - 5868585424054096000B8131 /* AppButton.swift */, + 7A5869962B32EA4500640D27 /* AppButton.swift */, 7A9FA1412A2E3306000B728D /* CheckboxView.swift */, + 5868585424054096000B8131 /* CustomButton.swift */, 58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */, 58ACF64E26567A7100ACE4B7 /* CustomSwitchContainer.swift */, 58293FB025124117005D0BB5 /* CustomTextField.swift */, @@ -2311,6 +2316,7 @@ 5892A45D265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift */, 58FD5BF32428C67600112C88 /* InAppPurchaseButton.swift */, F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */, + 7A5869942B32E9C700640D27 /* LinkButton.swift */, 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */, E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */, 58EF581025D69DB400AEBA94 /* StatusImageView.swift */, @@ -4616,6 +4622,7 @@ 5846227126E229F20035F7C2 /* StoreSubscription.swift in Sources */, 58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */, F0E8E4C92A604E7400ED26A3 /* AccountDeletionInteractor.swift in Sources */, + 7A5869952B32E9C700640D27 /* LinkButton.swift in Sources */, F09A297D2A9F8A9B00EA3B6F /* RedeemVoucherContentView.swift in Sources */, 5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */, 587EB672271451E300123C75 /* PreferencesViewModel.swift in Sources */, @@ -4636,6 +4643,7 @@ 58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */, 5878A27B2909649A0096FC88 /* CustomOverlayRenderer.swift in Sources */, A91614D62B10B26B00F416EB /* TunnelControlViewModel.swift in Sources */, + 7A5869972B32EA4500640D27 /* AppButton.swift in Sources */, 586C0D8F2B03D88100E7CDD7 /* ProxyProtocolConfigurationItemIdentifier.swift in Sources */, 588527B2276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift in Sources */, 58DFF7D22B0256A300F864E0 /* MarkdownStylingOptions.swift in Sources */, @@ -4744,7 +4752,7 @@ 5827B0AE2B0F4CBE00CCBBA1 /* ProxyConfigurationViewControllerDelegate.swift in Sources */, 5827B0962B0DB2C100CCBBA1 /* ProxyConfigurationItemIdentifier.swift in Sources */, 586C0D8B2B03D84400E7CDD7 /* AddAccessMethodItemIdentifier.swift in Sources */, - 5868585524054096000B8131 /* AppButton.swift in Sources */, + 5868585524054096000B8131 /* CustomButton.swift in Sources */, 58E25F812837BBBB002CFB2C /* SceneDelegate.swift in Sources */, 7A1A26492A29D48A00B978AA /* RelayFilterCellFactory.swift in Sources */, 5867771629097C5B006F721F /* ProductState.swift in Sources */, diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift index 29689a7dcd81..6d8ba9b92dae 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift @@ -72,7 +72,7 @@ class ListAccessMethodViewController: UIViewController, UITableViewDelegate { addChild(contentController) contentController.didMove(toParent: self) - interactor.publisher.sink { newElements in + interactor.publisher.sink { _ in self.updateDataSource(animated: true) } .store(in: &cancellables) @@ -104,7 +104,7 @@ class ListAccessMethodViewController: UIViewController, UITableViewDelegate { private func configureDataSource() { dataSource = UITableViewDiffableDataSource( tableView: tableView, - cellProvider: { [weak self] tableView, indexPath, itemIdentifier in + cellProvider: { [weak self] _, indexPath, itemIdentifier in self?.dequeueCell(at: indexPath, itemIdentifier: itemIdentifier) } ) diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift b/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift index 5a7e40b8e639..6955c3870dbf 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift @@ -25,6 +25,7 @@ struct SettingsCellFactory: CellFactoryProtocol { return cell } + // swiftlint:disable:next function_body_length func configureCell(_ cell: UITableViewCell, item: SettingsDataSource.Item, indexPath: IndexPath) { switch item { case .preferences: diff --git a/ios/MullvadVPN/Views/AppButton.swift b/ios/MullvadVPN/Views/AppButton.swift index c63ac8aaca65..245a0f5f9a39 100644 --- a/ios/MullvadVPN/Views/AppButton.swift +++ b/ios/MullvadVPN/Views/AppButton.swift @@ -8,96 +8,6 @@ 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 -} - -private extension UIControl.State { - var customButtonTitleColor: UIColor? { - switch self { - case .normal: - return UIColor.AppButton.normalTitleColor - case .disabled: - return UIColor.AppButton.disabledTitleColor.withAlphaComponent(0.5) - case .highlighted: - return UIColor.AppButton.highlightedTitleColor - default: - return nil - } - } -} - -/// A subclass that implements the button that visually look like URL links on the web -class LinkButton: CustomButton { - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - var titleString: String? { - didSet { - updateAttributedTitle(string: titleString) - } - } - - private func commonInit() { - imageAlignment = .trailing - contentHorizontalAlignment = .leading - - accessibilityTraits.insert(.link) - } - - private func updateAttributedTitle(string: String?) { - let states: [UIControl.State] = [.normal, .highlighted, .disabled] - states.forEach { state in - let attributedTitle = string.flatMap { makeAttributedTitle($0, for: state) } - self.setAttributedTitle(attributedTitle, for: state) - } - } - - private func makeAttributedTitle( - _ title: String, - for state: UIControl.State - ) -> NSAttributedString { - var attributes: [NSAttributedString.Key: Any] = [ - .underlineStyle: NSUnderlineStyle.single.rawValue, - ] - - if let titleColor = state.customButtonTitleColor { - attributes[.foregroundColor] = titleColor - } - - return NSAttributedString(string: title, attributes: attributes) - } -} - /// A subclass that implements action buttons used across the app class AppButton: CustomButton { /// Default content insets based on current trait collection. @@ -260,146 +170,6 @@ class AppButton: CustomButton { } } -/// A custom `UIButton` subclass that implements additional layouts for the image -class CustomButton: UIButton { - var imageAlignment: ButtonImageAlignment = .leading { - didSet { - invalidateIntrinsicContentSize() - } - } - - var inlineImageSpacing: CGFloat = 4 { - didSet { - invalidateIntrinsicContentSize() - } - } - - 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 - } - - 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() - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - 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 - } -} - private extension AppButton { class DynamicAssets { static let shared = DynamicAssets() diff --git a/ios/MullvadVPN/Views/CustomButton.swift b/ios/MullvadVPN/Views/CustomButton.swift new file mode 100644 index 000000000000..46353bb1a607 --- /dev/null +++ b/ios/MullvadVPN/Views/CustomButton.swift @@ -0,0 +1,190 @@ +// +// CustomButton.swift +// MullvadVPN +// +// Created by pronebird on 23/05/2019. +// Copyright © 2019 Mullvad VPN AB. All rights reserved. +// + +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) + case .highlighted: + return UIColor.AppButton.highlightedTitleColor + default: + return nil + } + } +} + +/// A custom `UIButton` subclass that implements additional layouts for the image +class CustomButton: UIButton { + var imageAlignment: ButtonImageAlignment = .leading { + didSet { + invalidateIntrinsicContentSize() + } + } + + var inlineImageSpacing: CGFloat = 4 { + didSet { + invalidateIntrinsicContentSize() + } + } + + 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 + } + + 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() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + 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 + } +} diff --git a/ios/MullvadVPN/Views/LinkButton.swift b/ios/MullvadVPN/Views/LinkButton.swift new file mode 100644 index 000000000000..ebd3be70893b --- /dev/null +++ b/ios/MullvadVPN/Views/LinkButton.swift @@ -0,0 +1,58 @@ +// +// LinkButton.swift +// MullvadVPN +// +// Created by Jon Petersson on 2023-12-20. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +/// A subclass that implements the button that visually look like URL links on the web +class LinkButton: CustomButton { + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + var titleString: String? { + didSet { + updateAttributedTitle(string: titleString) + } + } + + private func commonInit() { + imageAlignment = .trailing + contentHorizontalAlignment = .leading + + accessibilityTraits.insert(.link) + } + + private func updateAttributedTitle(string: String?) { + let states: [UIControl.State] = [.normal, .highlighted, .disabled] + states.forEach { state in + let attributedTitle = string.flatMap { makeAttributedTitle($0, for: state) } + self.setAttributedTitle(attributedTitle, for: state) + } + } + + private func makeAttributedTitle( + _ title: String, + for state: UIControl.State + ) -> NSAttributedString { + var attributes: [NSAttributedString.Key: Any] = [ + .underlineStyle: NSUnderlineStyle.single.rawValue, + ] + + if let titleColor = state.customButtonTitleColor { + attributes[.foregroundColor] = titleColor + } + + return NSAttributedString(string: title, attributes: attributes) + } +} diff --git a/ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift b/ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift index ae815f369200..a12a12277224 100644 --- a/ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift +++ b/ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift @@ -49,10 +49,13 @@ struct WgAdapter: TunnelAdapterProtocol { } private func logIfDeviceHasSameIP(than addresses: [IPAddress]) { + let sameIPv4 = IPv4Address("10.127.255.254") + let sameIPv6 = IPv6Address("fc00:bbbb:bbbb:bb01:ffff:ffff:ffff:ffff") + let hasIPv4SameAddress = addresses.compactMap { $0 as? IPv4Address } - .contains { $0 == ApplicationConfiguration.sameIPv4 } + .contains { $0 == sameIPv4 } let hasIPv6SameAddress = addresses.compactMap { $0 as? IPv6Address } - .contains { $0 == ApplicationConfiguration.sameIPv6 } + .contains { $0 == sameIPv6 } let isUsingSameIP = (hasIPv4SameAddress || hasIPv6SameAddress) ? "" : "NOT " logger.debug("Same IP is \(isUsingSameIP)being used") diff --git a/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift b/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift index b80d92df95b7..1e19bde6c3a6 100644 --- a/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift +++ b/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift @@ -214,22 +214,20 @@ final class PacketTunnelActorTests: XCTestCase { let actor = PacketTunnelActor.mock(blockedStateErrorMapper: blockedStateMapper, settingsReader: settingsReader) - stateSink = await actor.$observedState - .receive(on: DispatchQueue.main) - .sink { newState in - switch newState { - case .initial: - initialStateExpectation.fulfill() - case .error: - errorStateExpectation.fulfill() - case .connecting: - connectingStateExpectation.fulfill() - case .connected: - connectedStateExpectation.fulfill() - default: - break - } + stateSink = await actor.$observedState.receive(on: DispatchQueue.main).sink { newState in + switch newState { + case .initial: + initialStateExpectation.fulfill() + case .error: + errorStateExpectation.fulfill() + case .connecting: + connectingStateExpectation.fulfill() + case .connected: + connectedStateExpectation.fulfill() + default: + break } + } actor.start(options: launchOptions) diff --git a/ios/Shared/ApplicationConfiguration.swift b/ios/Shared/ApplicationConfiguration.swift index a3af1eef75f9..426067e1dd63 100644 --- a/ios/Shared/ApplicationConfiguration.swift +++ b/ios/Shared/ApplicationConfiguration.swift @@ -37,10 +37,4 @@ enum ApplicationConfiguration { /// Maximum number of devices per account. static let maxAllowedDevices = 5 - - // FIXME: Used for debugging purposes during the migration to same IP. Remove when the migration is over. - // swiftlint disable:force_cast - static let sameIPv4 = IPv4Address("10.127.255.254")! - static let sameIPv6 = IPv6Address("fc00:bbbb:bbbb:bb01:ffff:ffff:ffff:ffff")! - // swiftlint enable:force_cast }