From 80eb27b57db7ffc9b6f623022e836ed34ac14c5f Mon Sep 17 00:00:00 2001 From: Jon Petersson Date: Tue, 15 Oct 2024 10:46:10 +0200 Subject: [PATCH] Update cell layout and behavior for obfuscation methods --- ios/MullvadVPN.xcodeproj/project.pbxproj | 10 +- .../Classes/AccessbilityIdentifier.swift | 3 +- ios/MullvadVPN/UI appearance/UIMetrics.swift | 2 + .../Settings/CheckableSettingsCell.swift | 18 +++- .../Settings/SelectableSettingsCell.swift | 17 +++- .../SelectableSettingsDetailsCell.swift | 73 ++++++++++++++ .../Settings/SettingsAddDNSEntryCell.swift | 4 +- .../Settings/SettingsCell.swift | 95 +++++++++++-------- .../VPNSettings/CustomDNSCellFactory.swift | 2 +- .../VPNSettings/VPNSettingsCellFactory.swift | 58 +++++++++-- .../VPNSettings/VPNSettingsDataSource.swift | 46 +++++++-- .../VPNSettingsDataSourceDelegate.swift | 1 + .../VPNSettingsDetailsButtonItem.swift | 12 +++ .../VPNSettingsInfoButtonItem.swift | 6 +- .../VPNSettingsViewController.swift | 5 + .../Pages/VPNSettingsPage.swift | 4 +- 16 files changed, 282 insertions(+), 74 deletions(-) create mode 100644 ios/MullvadVPN/View controllers/Settings/SelectableSettingsDetailsCell.swift create mode 100644 ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDetailsButtonItem.swift diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 2bd37ffa6cc1..7fe94e8467cf 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -466,6 +466,8 @@ 7A27E3C92CAE85710088BCFF /* SettingsInfoButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27E3C82CAE85660088BCFF /* SettingsInfoButtonItem.swift */; }; 7A27E3CB2CAE861D0088BCFF /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27E3CA2CAE86170088BCFF /* SettingsViewModel.swift */; }; 7A27E3CD2CB814EF0088BCFF /* DAITAInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27E3CC2CB814EA0088BCFF /* DAITAInfoView.swift */; }; + 7A27E3CF2CBD4A8C0088BCFF /* SelectableSettingsDetailsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27E3CE2CBD4A830088BCFF /* SelectableSettingsDetailsCell.swift */; }; + 7A27E3D12CC299F90088BCFF /* VPNSettingsDetailsButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27E3D02CC299E60088BCFF /* VPNSettingsDetailsButtonItem.swift */; }; 7A28826A2BA8336600FD9F20 /* VPNSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2882692BA8336600FD9F20 /* VPNSettingsCoordinator.swift */; }; 7A2960F62A963F7500389B82 /* AlertCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2960F52A963F7500389B82 /* AlertCoordinator.swift */; }; 7A2960FD2A964BB700389B82 /* AlertPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2960FC2A964BB700389B82 /* AlertPresentation.swift */; }; @@ -1803,6 +1805,8 @@ 7A27E3C82CAE85660088BCFF /* SettingsInfoButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInfoButtonItem.swift; sourceTree = ""; }; 7A27E3CA2CAE86170088BCFF /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; 7A27E3CC2CB814EA0088BCFF /* DAITAInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAITAInfoView.swift; sourceTree = ""; }; + 7A27E3CE2CBD4A830088BCFF /* SelectableSettingsDetailsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableSettingsDetailsCell.swift; sourceTree = ""; }; + 7A27E3D02CC299E60088BCFF /* VPNSettingsDetailsButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSettingsDetailsButtonItem.swift; sourceTree = ""; }; 7A2882692BA8336600FD9F20 /* VPNSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSettingsCoordinator.swift; sourceTree = ""; }; 7A2960F52A963F7500389B82 /* AlertCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertCoordinator.swift; sourceTree = ""; }; 7A2960FC2A964BB700389B82 /* AlertPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPresentation.swift; sourceTree = ""; }; @@ -2802,7 +2806,9 @@ isa = PBXGroup; children = ( 7A9FA1432A2E3FE5000B728D /* CheckableSettingsCell.swift */, + F041BE4E2C983C2B0083EC28 /* DAITASettingsPromptItem.swift */, 7A1A264A2A29D65E00B978AA /* SelectableSettingsCell.swift */, + 7A27E3CE2CBD4A830088BCFF /* SelectableSettingsDetailsCell.swift */, 5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */, 582BB1AE229566420055B6EF /* SettingsCell.swift */, 5864AF0029C7879B005B0CD9 /* SettingsCellFactory.swift */, @@ -2815,7 +2821,6 @@ 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */, 58677711290976FB006F721F /* SettingsInteractor.swift */, 5867770F290975E8006F721F /* SettingsInteractorFactory.swift */, - F041BE4E2C983C2B0083EC28 /* DAITASettingsPromptItem.swift */, 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */, 58CCA01122424D11004F3011 /* SettingsViewController.swift */, 7A27E3CA2CAE86170088BCFF /* SettingsViewModel.swift */, @@ -2845,6 +2850,7 @@ 5864AF0229C7879B005B0CD9 /* VPNSettingsCellFactory.swift */, 584D26C3270C855A004EA533 /* VPNSettingsDataSource.swift */, 587EB6732714520600123C75 /* VPNSettingsDataSourceDelegate.swift */, + 7A27E3D02CC299E60088BCFF /* VPNSettingsDetailsButtonItem.swift */, 7A6F2FAE2AFE36E7006D0856 /* VPNSettingsInfoButtonItem.swift */, 5871167E2910035700D41AAC /* VPNSettingsInteractor.swift */, 58ACF6482655365700ACE4B7 /* VPNSettingsViewController.swift */, @@ -5658,6 +5664,7 @@ A91614D62B10B26B00F416EB /* TunnelControlViewModel.swift in Sources */, 7A5869972B32EA4500640D27 /* AppButton.swift in Sources */, 586C0D8F2B03D88100E7CDD7 /* ProxyProtocolConfigurationItemIdentifier.swift in Sources */, + 7A27E3CF2CBD4A8C0088BCFF /* SelectableSettingsDetailsCell.swift in Sources */, 7A27E3CB2CAE861D0088BCFF /* SettingsViewModel.swift in Sources */, 588527B2276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift in Sources */, 7A9F29392CABFAFC005F2089 /* InfoHeaderView.swift in Sources */, @@ -5854,6 +5861,7 @@ F0DA87492A9CBA9F006044F1 /* AccountDeviceRow.swift in Sources */, 58FF9FE42B075BDD00E4C97D /* EditAccessMethodItemIdentifier.swift in Sources */, 5878A27329091D6D0096FC88 /* TunnelBlockObserver.swift in Sources */, + 7A27E3D12CC299F90088BCFF /* VPNSettingsDetailsButtonItem.swift in Sources */, A9E034642ABB302000E59A5A /* UIEdgeInsets+Extensions.swift in Sources */, 58CEB2E92AFBBA4A00E6E088 /* AddAccessMethodCoordinator.swift in Sources */, 58DFF7D02B02560400F864E0 /* NSAttributedString+Extensions.swift in Sources */, diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift index 6cfbce70a131..c706fd9298cb 100644 --- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift +++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift @@ -174,7 +174,8 @@ public enum AccessibilityIdentifier: String { case wireGuardObfuscationAutomatic case wireGuardObfuscationPort case wireGuardObfuscationOff - case wireGuardObfuscationOn + case wireGuardObfuscationUdpOverTcp + case wireGuardObfuscationShadowsocks case wireGuardPort // Custom DNS diff --git a/ios/MullvadVPN/UI appearance/UIMetrics.swift b/ios/MullvadVPN/UI appearance/UIMetrics.swift index 9e4997da1ea4..ce108dceb84d 100644 --- a/ios/MullvadVPN/UI appearance/UIMetrics.swift +++ b/ios/MullvadVPN/UI appearance/UIMetrics.swift @@ -93,6 +93,8 @@ enum UIMetrics { static let customListsCellHeight: CGFloat = 44 static let apiAccessSwitchCellTrailingMargin: CGFloat = apiAccessInsetLayoutMargins.trailing - 4 static let apiAccessPickerListContentInsetTop: CGFloat = 16 + static let verticalDividerHeight: CGFloat = 22 + static let detailsButtonSize: CGFloat = 60 } enum InAppBannerNotification { diff --git a/ios/MullvadVPN/View controllers/Settings/CheckableSettingsCell.swift b/ios/MullvadVPN/View controllers/Settings/CheckableSettingsCell.swift index a732234473f5..6f7b0cb5ef9d 100644 --- a/ios/MullvadVPN/View controllers/Settings/CheckableSettingsCell.swift +++ b/ios/MullvadVPN/View controllers/Settings/CheckableSettingsCell.swift @@ -14,7 +14,7 @@ class CheckableSettingsCell: SettingsCell { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - setLeftView(checkboxView, spacing: UIMetrics.SettingsCell.checkableSettingsCellLeftViewSpacing) + setCheckboxView() selectedBackgroundView?.backgroundColor = .clear } @@ -24,8 +24,7 @@ class CheckableSettingsCell: SettingsCell { override func prepareForReuse() { super.prepareForReuse() - - setLeftView(checkboxView, spacing: UIMetrics.SettingsCell.checkableSettingsCellLeftViewSpacing) + setCheckboxView() } override func setSelected(_ selected: Bool, animated: Bool) { @@ -39,4 +38,17 @@ class CheckableSettingsCell: SettingsCell { contentView.layoutMargins.left = 0 } + + private func setCheckboxView() { + setLeadingView { superview in + superview.addConstrainedSubviews([checkboxView]) { + checkboxView.pinEdgesToSuperview(PinnableEdges([ + .leading(0), + .trailing(UIMetrics.SettingsCell.checkableSettingsCellLeftViewSpacing), + .top(0), + .bottom(0), + ])) + } + } + } } diff --git a/ios/MullvadVPN/View controllers/Settings/SelectableSettingsCell.swift b/ios/MullvadVPN/View controllers/Settings/SelectableSettingsCell.swift index 3cc5bb1e76f3..9da9564d27ed 100644 --- a/ios/MullvadVPN/View controllers/Settings/SelectableSettingsCell.swift +++ b/ios/MullvadVPN/View controllers/Settings/SelectableSettingsCell.swift @@ -11,6 +11,7 @@ import UIKit class SelectableSettingsCell: SettingsCell { let tickImageView: UIImageView = { let imageView = UIImageView(image: UIImage(named: "IconTick")) + imageView.contentMode = .center imageView.tintColor = .white imageView.alpha = 0 return imageView @@ -19,7 +20,7 @@ class SelectableSettingsCell: SettingsCell { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - setLeftView(tickImageView, spacing: UIMetrics.SettingsCell.selectableSettingsCellLeftViewSpacing) + setTickView() selectedBackgroundView?.backgroundColor = UIColor.Cell.Background.selected } @@ -29,8 +30,7 @@ class SelectableSettingsCell: SettingsCell { override func prepareForReuse() { super.prepareForReuse() - - setLeftView(tickImageView, spacing: UIMetrics.SettingsCell.selectableSettingsCellLeftViewSpacing) + setTickView() } override func setSelected(_ selected: Bool, animated: Bool) { @@ -38,4 +38,15 @@ class SelectableSettingsCell: SettingsCell { tickImageView.alpha = selected ? 1 : 0 } + + private func setTickView() { + setLeadingView { superview in + superview.addConstrainedSubviews([tickImageView]) { + tickImageView.pinEdgesToSuperview(PinnableEdges([ + .leading(0), .top(0), .bottom(0), + .trailing(UIMetrics.SettingsCell.selectableSettingsCellLeftViewSpacing), + ])) + } + } + } } diff --git a/ios/MullvadVPN/View controllers/Settings/SelectableSettingsDetailsCell.swift b/ios/MullvadVPN/View controllers/Settings/SelectableSettingsDetailsCell.swift new file mode 100644 index 000000000000..33708d1b86cd --- /dev/null +++ b/ios/MullvadVPN/View controllers/Settings/SelectableSettingsDetailsCell.swift @@ -0,0 +1,73 @@ +// +// SelectableSettingsDetailsCell.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-10-14. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +class SelectableSettingsDetailsCell: SelectableSettingsCell { + let viewContainer = UIView() + + var buttonAction: (() -> Void)? + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) + + let actionButton = IncreasedHitButton(type: .system) + var actionButtonConfiguration = actionButton.configuration ?? .plain() + actionButtonConfiguration.image = UIImage(systemName: "ellipsis")? + .withRenderingMode(.alwaysOriginal) + .withTintColor(.white) + actionButton.configuration = actionButtonConfiguration + + actionButton.addTarget( + self, + action: #selector(didPressActionButton), + for: .touchUpInside + ) + + let separatorView = UIView() + separatorView.backgroundColor = .secondaryColor + + viewContainer.addConstrainedSubviews([separatorView, actionButton]) { + separatorView.leadingAnchor.constraint(equalTo: viewContainer.leadingAnchor, constant: 16) + separatorView.centerYAnchor.constraint(equalTo: viewContainer.centerYAnchor) + separatorView.heightAnchor.constraint(equalToConstant: UIMetrics.SettingsCell.verticalDividerHeight) + separatorView.widthAnchor.constraint(equalToConstant: 1) + + actionButton.pinEdgesToSuperview(.all().excluding(.leading)) + actionButton.leadingAnchor.constraint(equalTo: separatorView.trailingAnchor) + actionButton.widthAnchor.constraint(equalToConstant: UIMetrics.SettingsCell.detailsButtonSize) + } + + setViewContainer() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + setViewContainer() + } + + private func setViewContainer() { + #if DEBUG + setTrailingView { superview in + superview.addConstrainedSubviews([viewContainer]) { + viewContainer.pinEdgesToSuperview() + } + } + #endif + } + + // MARK: - Actions + + @objc private func didPressActionButton() { + buttonAction?() + } +} diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsAddDNSEntryCell.swift b/ios/MullvadVPN/View controllers/Settings/SettingsAddDNSEntryCell.swift index 11f2c416bcdd..51f7131ff963 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsAddDNSEntryCell.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsAddDNSEntryCell.swift @@ -9,7 +9,7 @@ import UIKit class SettingsAddDNSEntryCell: SettingsCell { - var action: (() -> Void)? + var tapAction: (() -> Void)? override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -29,7 +29,7 @@ class SettingsAddDNSEntryCell: SettingsCell { @objc func handleTap(_ sender: UIGestureRecognizer) { if case .ended = sender.state { - action?() + tapAction?() } } } diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift b/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift index c55b5f0aa2f8..591857be9bc8 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift @@ -31,11 +31,10 @@ enum SettingsDisclosureType { class SettingsCell: UITableViewCell, CustomCellDisclosureHandling { typealias InfoButtonHandler = () -> Void - let contentContainerSubviewMaxCount = 2 - let titleLabel = UILabel() - let detailTitleLabel = UILabel() let disclosureImageView = UIImageView(image: nil) - let contentContainer = UIStackView() + let mainContentContainer = UIView() + let leftContentContainer = UIView() + let rightContentContainer = UIView() var infoButtonHandler: InfoButtonHandler? { didSet { infoButton.isHidden = infoButtonHandler == nil }} @@ -59,8 +58,27 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling { } } + let titleLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 17) + label.textColor = UIColor.Cell.titleTextColor + label.setContentHuggingPriority(.defaultHigh, for: .horizontal) + label.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + return label + }() + + let detailTitleLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 13) + label.textColor = UIColor.Cell.detailTextColor + label.setContentHuggingPriority(.defaultLow, for: .horizontal) + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + return label + }() + private var subCellLeadingIndentation: CGFloat = 0 private let buttonWidth: CGFloat = 24 + private let infoButton: UIButton = { let button = UIButton(type: .custom) button.accessibilityIdentifier = .infoButton @@ -83,38 +101,29 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling { backgroundColor = .clear contentView.backgroundColor = .clear - infoButton.isHidden = true infoButton.addTarget(self, action: #selector(handleInfoButton(_:)), for: .touchUpInside) subCellLeadingIndentation = contentView.layoutMargins.left + UIMetrics.TableView.cellIndentationWidth - titleLabel.translatesAutoresizingMaskIntoConstraints = false - titleLabel.font = UIFont.systemFont(ofSize: 17) - titleLabel.textColor = UIColor.Cell.titleTextColor - - detailTitleLabel.translatesAutoresizingMaskIntoConstraints = false - detailTitleLabel.font = UIFont.systemFont(ofSize: 13) - detailTitleLabel.textColor = UIColor.Cell.detailTextColor - - titleLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) - detailTitleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) - - titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - detailTitleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + rightContentContainer.setContentHuggingPriority(.required, for: .horizontal) setLayoutMargins() let buttonAreaWidth = UIMetrics.contentLayoutMargins.leading + UIMetrics .contentLayoutMargins.trailing + buttonWidth - let content = UIView() - content.addConstrainedSubviews([titleLabel, infoButton, detailTitleLabel]) { + let infoButtonConstraint = infoButton.trailingAnchor.constraint( + greaterThanOrEqualTo: mainContentContainer.trailingAnchor + ) + infoButtonConstraint.priority = .defaultLow + + mainContentContainer.addConstrainedSubviews([titleLabel, infoButton, detailTitleLabel]) { switch style { case .subtitle: titleLabel.pinEdgesToSuperview(.init([.top(0), .leading(0)])) - detailTitleLabel.pinEdgesToSuperview(.all().excluding(.top)) - detailTitleLabel.topAnchor.constraint(equalToSystemSpacingBelow: titleLabel.bottomAnchor, multiplier: 1) - infoButton.trailingAnchor.constraint(greaterThanOrEqualTo: content.trailingAnchor) + detailTitleLabel.pinEdgesToSuperview(.all().excluding([.top, .trailing])) + detailTitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor) + infoButtonConstraint default: titleLabel.pinEdgesToSuperview(.all().excluding(.trailing)) @@ -122,8 +131,6 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling { detailTitleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: infoButton.trailingAnchor) } - infoButton.pinEdgesToSuperview(.init([.top(0)])) - infoButton.bottomAnchor.constraint(lessThanOrEqualTo: content.bottomAnchor) infoButton.leadingAnchor.constraint( equalTo: titleLabel.trailingAnchor, constant: -UIMetrics.interButtonSpacing @@ -132,10 +139,17 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling { infoButton.widthAnchor.constraint(equalToConstant: buttonAreaWidth) } - contentContainer.addArrangedSubview(content) + contentView.addConstrainedSubviews([leftContentContainer, mainContentContainer, rightContentContainer]) { + mainContentContainer.pinEdgesToSuperviewMargins(.all().excluding([.leading, .trailing])) - contentView.addConstrainedSubviews([contentContainer]) { - contentContainer.pinEdgesToSuperviewMargins() + leftContentContainer.pinEdgesToSuperviewMargins(.all().excluding(.trailing)) + leftContentContainer.trailingAnchor.constraint(equalTo: mainContentContainer.leadingAnchor) + + rightContentContainer.pinEdgesToSuperview(.all().excluding(.leading)) + rightContentContainer.leadingAnchor.constraint(equalTo: mainContentContainer.trailingAnchor) + rightContentContainer.widthAnchor.constraint( + greaterThanOrEqualToConstant: UIMetrics.TableView.cellIndentationWidth + ) } } @@ -147,7 +161,8 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling { super.prepareForReuse() infoButton.isHidden = true - removeLeftView() + removeLeadingView() + removeTrailingView() setLayoutMargins() } @@ -156,20 +171,22 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling { backgroundView?.backgroundColor = UIColor.Cell.Background.indentationLevelOne } - func setLeftView(_ view: UIView, spacing: CGFloat) { - removeLeftView() + func setLeadingView(superviewProvider: (UIView) -> Void) { + removeLeadingView() + superviewProvider(leftContentContainer) + } - if contentContainer.arrangedSubviews.count <= 1 { - contentContainer.insertArrangedSubview(view, at: 0) - } + func removeLeadingView() { + leftContentContainer.subviews.forEach { $0.removeFromSuperview() } + } - contentContainer.spacing = spacing + func setTrailingView(superviewProvider: (UIView) -> Void) { + removeTrailingView() + superviewProvider(rightContentContainer) } - func removeLeftView() { - if contentContainer.arrangedSubviews.count >= contentContainerSubviewMaxCount { - contentContainer.arrangedSubviews.first?.removeFromSuperview() - } + func removeTrailingView() { + rightContentContainer.subviews.forEach { $0.removeFromSuperview() } } @objc private func handleInfoButton(_ sender: UIControl) { diff --git a/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSCellFactory.swift b/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSCellFactory.swift index 639fa2d17b90..b8c0c7c0981c 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSCellFactory.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSCellFactory.swift @@ -191,7 +191,7 @@ final class CustomDNSCellFactory: CellFactoryProtocol { comment: "" ) cell.accessibilityIdentifier = .dnsSettingsAddServerCell - cell.action = { [weak self] in + cell.tapAction = { [weak self] in self?.delegate?.addDNSEntry() } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift index 16d6ff24a5cf..51038d4b68c0 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift @@ -11,6 +11,7 @@ import UIKit protocol VPNSettingsCellEventHandler { func showInfo(for button: VPNSettingsInfoButtonItem) + func showDetails(for button: VPNSettingsDetailsButtonItem) func addCustomPort(_ port: UInt16) func selectCustomPortEntry(_ port: UInt16) -> Bool func selectObfuscationState(_ state: WireGuardObfuscationState) @@ -69,7 +70,7 @@ final class VPNSettingsCellFactory: CellFactoryProtocol { guard let cell = cell as? SelectableSettingsCell else { return } var portString = NSLocalizedString( - "WIRE_GUARD_PORT_CELL_LABEL", + "WIREGUARD_PORT_CELL_LABEL", tableName: "VPNSettings", value: "Automatic", comment: "" @@ -86,13 +87,13 @@ final class VPNSettingsCellFactory: CellFactoryProtocol { guard let cell = cell as? SettingsInputCell else { return } cell.titleLabel.text = NSLocalizedString( - "WIRE_GUARD_CUSTOM_PORT_CELL_LABEL", + "WIREGUARD_CUSTOM_PORT_CELL_LABEL", tableName: "VPNSettings", value: "Custom", comment: "" ) cell.textField.placeholder = NSLocalizedString( - "WIRE_GUARD_CUSTOM_PORT_CELL_INPUT_PLACEHOLDER", + "WIREGUARD_CUSTOM_PORT_CELL_INPUT_PLACEHOLDER", tableName: "VPNSettings", value: "Port", comment: "" @@ -127,7 +128,7 @@ final class VPNSettingsCellFactory: CellFactoryProtocol { guard let cell = cell as? SelectableSettingsCell else { return } cell.titleLabel.text = NSLocalizedString( - "WIRE_GUARD_OBFUSCATION_AUTOMATIC_LABEL", + "WIREGUARD_OBFUSCATION_AUTOMATIC_LABEL", tableName: "VPNSettings", value: "Automatic", comment: "" @@ -135,22 +136,59 @@ final class VPNSettingsCellFactory: CellFactoryProtocol { cell.accessibilityIdentifier = item.accessibilityIdentifier cell.applySubCellStyling() - case .wireGuardObfuscationOn: - guard let cell = cell as? SelectableSettingsCell else { return } + case .wireGuardObfuscationUdpOverTcp: + guard let cell = cell as? SelectableSettingsDetailsCell else { return } + + cell.titleLabel.text = NSLocalizedString( + "WIREGUARD_OBFUSCATION_UDP_TCP_LABEL", + tableName: "VPNSettings", + value: "UDP-over-TCP", + comment: "" + ) + #if DEBUG + cell.detailTitleLabel.text = String(format: NSLocalizedString( + "WIREGUARD_OBFUSCATION_UDP_TCP_PORT", + tableName: "VPNSettings", + value: "Port: %d", + comment: "" + ), viewModel.obfuscationPort.portValue) + #endif + cell.accessibilityIdentifier = item.accessibilityIdentifier + cell.applySubCellStyling() + + cell.buttonAction = { [weak self] in + self?.delegate?.showDetails(for: .udpOverTcp) + } + + case .wireGuardObfuscationShadowsocks: + guard let cell = cell as? SelectableSettingsDetailsCell else { return } cell.titleLabel.text = NSLocalizedString( - "WIRE_GUARD_OBFUSCATION_ON_LABEL", + "WIREGUARD_OBFUSCATION_SHADOWSOCKS_LABEL", tableName: "VPNSettings", - value: "On (UDP-over-TCP)", + value: "Shadowsocks", comment: "" ) + #if DEBUG + cell.detailTitleLabel.text = String(format: NSLocalizedString( + "WIREGUARD_OBFUSCATION_SHADOWSOCKS_PORT", + tableName: "VPNSettings", + value: "Port: %d", + comment: "" + ), viewModel.obfuscationPort.portValue) + #endif cell.accessibilityIdentifier = item.accessibilityIdentifier cell.applySubCellStyling() + + cell.buttonAction = { [weak self] in + self?.delegate?.showDetails(for: .wireguardOverShadowsocks) + } + case .wireGuardObfuscationOff: guard let cell = cell as? SelectableSettingsCell else { return } cell.titleLabel.text = NSLocalizedString( - "WIRE_GUARD_OBFUSCATION_OFF_LABEL", + "WIREGUARD_OBFUSCATION_OFF_LABEL", tableName: "VPNSettings", value: "Off", comment: "" @@ -163,7 +201,7 @@ final class VPNSettingsCellFactory: CellFactoryProtocol { let portString = port == 0 ? "Automatic" : "\(port)" cell.titleLabel.text = NSLocalizedString( - "WIRE_GUARD_OBFUSCATION_PORT_LABEL", + "WIREGUARD_OBFUSCATION_PORT_LABEL", tableName: "VPNSettings", value: portString, comment: "" diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift index 2aa5f6dd5679..5c0f8856d3ac 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift @@ -21,6 +21,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case wireGuardPort case wireGuardCustomPort case wireGuardObfuscation + case wireGuardObfuscationOption case wireGuardObfuscationPort case quantumResistance case multihop @@ -35,6 +36,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< return SelectableSettingsCell.self case .wireGuardCustomPort: return SettingsInputCell.self + case .wireGuardObfuscationOption: + return SelectableSettingsDetailsCell.self case .wireGuardObfuscation: return SelectableSettingsCell.self case .wireGuardObfuscationPort: @@ -73,7 +76,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case wireGuardPort(_ port: UInt16?) case wireGuardCustomPort case wireGuardObfuscationAutomatic - case wireGuardObfuscationOn + case wireGuardObfuscationUdpOverTcp + case wireGuardObfuscationShadowsocks case wireGuardObfuscationOff case wireGuardObfuscationPort(_ port: UInt16) case quantumResistanceAutomatic @@ -89,11 +93,21 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< } static var wireGuardObfuscation: [Item] { - [.wireGuardObfuscationAutomatic, .wireGuardObfuscationOn, wireGuardObfuscationOff] + var items: [Item] = [ + .wireGuardObfuscationAutomatic, + .wireGuardObfuscationUdpOverTcp, + .wireGuardObfuscationOff, + ] + + #if DEBUG + items.insert(.wireGuardObfuscationShadowsocks, at: 1) + #endif + + return items } static var wireGuardObfuscationPort: [Item] { - [.wireGuardObfuscationPort(0), wireGuardObfuscationPort(80), wireGuardObfuscationPort(5001)] + [.wireGuardObfuscationPort(0), .wireGuardObfuscationPort(80), .wireGuardObfuscationPort(5001)] } static var quantumResistance: [Item] { @@ -112,8 +126,10 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< return .wireGuardCustomPort case .wireGuardObfuscationAutomatic: return .wireGuardObfuscationAutomatic - case .wireGuardObfuscationOn: - return .wireGuardObfuscationOn + case .wireGuardObfuscationUdpOverTcp: + return .wireGuardObfuscationUdpOverTcp + case .wireGuardObfuscationShadowsocks: + return .wireGuardObfuscationShadowsocks case .wireGuardObfuscationOff: return .wireGuardObfuscationOff case .wireGuardObfuscationPort: @@ -139,8 +155,10 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< return .wireGuardPort case .wireGuardCustomPort: return .wireGuardCustomPort - case .wireGuardObfuscationAutomatic, .wireGuardObfuscationOn, .wireGuardObfuscationOff: + case .wireGuardObfuscationAutomatic, .wireGuardObfuscationOff: return .wireGuardObfuscation + case .wireGuardObfuscationUdpOverTcp, .wireGuardObfuscationShadowsocks: + return .wireGuardObfuscationOption case .wireGuardObfuscationPort: return .wireGuardObfuscationPort case .quantumResistanceAutomatic, .quantumResistanceOn, .quantumResistanceOff: @@ -174,8 +192,9 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< let obfuscationStateItem: Item = switch viewModel.obfuscationState { case .automatic: .wireGuardObfuscationAutomatic case .off: .wireGuardObfuscationOff - case .on: .wireGuardObfuscationOn + case .on: .wireGuardObfuscationUdpOverTcp } + let quantumResistanceItem: Item = switch viewModel.quantumResistance { case .automatic: .quantumResistanceAutomatic case .off: .quantumResistanceOff @@ -288,9 +307,14 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case .wireGuardObfuscationAutomatic: selectObfuscationState(.automatic) delegate?.didUpdateTunnelSettings(TunnelSettingsUpdate.obfuscation(obfuscationSettings)) - case .wireGuardObfuscationOn: + case .wireGuardObfuscationUdpOverTcp: selectObfuscationState(.on) delegate?.didUpdateTunnelSettings(TunnelSettingsUpdate.obfuscation(obfuscationSettings)) + // TODO: When ready, add implementation for selected obfuscation. + case .wireGuardObfuscationShadowsocks: + selectObfuscationState(.on) + delegate?.didUpdateTunnelSettings(TunnelSettingsUpdate.obfuscation(obfuscationSettings)) + // TODO: When ready, add implementation for selected obfuscation. case .wireGuardObfuscationOff: selectObfuscationState(.off) delegate?.didUpdateTunnelSettings(TunnelSettingsUpdate.obfuscation(obfuscationSettings)) @@ -445,7 +469,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< private func configureWireguardPortsHeader(_ header: SettingsHeaderView) { let title = NSLocalizedString( - "WIRE_GUARD_PORTS_HEADER_LABEL", + "WIREGUARD_PORTS_HEADER_LABEL", tableName: "VPNSettings", value: "WireGuard ports", comment: "" @@ -610,6 +634,10 @@ extension VPNSettingsDataSource: VPNSettingsCellEventHandler { delegate?.showInfo(for: button) } + func showDetails(for button: VPNSettingsDetailsButtonItem) { + delegate?.showDetails(for: button) + } + func addCustomPort(_ port: UInt16) { viewModel.setWireGuardPort(port) delegate?.didSelectWireGuardPort(port) diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift index de1a602fce27..98695d1f7fed 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift @@ -17,6 +17,7 @@ protocol DNSSettingsDataSourceDelegate: AnyObject { protocol VPNSettingsDataSourceDelegate: AnyObject { func didUpdateTunnelSettings(_ update: TunnelSettingsUpdate) func showInfo(for: VPNSettingsInfoButtonItem) + func showDetails(for: VPNSettingsDetailsButtonItem) func showDNSSettings() func showIPOverrides() func didSelectWireGuardPort(_ port: UInt16?) diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDetailsButtonItem.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDetailsButtonItem.swift new file mode 100644 index 000000000000..1dc3121befcc --- /dev/null +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDetailsButtonItem.swift @@ -0,0 +1,12 @@ +// +// VPNSettingsDetailsButtonItem.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-10-18. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +enum VPNSettingsDetailsButtonItem { + case udpOverTcp + case wireguardOverShadowsocks +} diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift index ac57e39db1d5..472802d11e32 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInfoButtonItem.swift @@ -45,7 +45,7 @@ enum VPNSettingsInfoButtonItem: CustomStringConvertible { case let .wireGuardPorts(portsString): String( format: NSLocalizedString( - "VPN_SETTINGS_WIRE_GUARD_PORTS_GENERAL", + "VPN_SETTINGS_WIREGUARD_PORTS_GENERAL", tableName: "WireGuardPorts", value: """ The automatic setting will randomly choose from the valid port ranges shown below. @@ -58,7 +58,7 @@ enum VPNSettingsInfoButtonItem: CustomStringConvertible { ) case .wireGuardObfuscation: NSLocalizedString( - "VPN_SETTINGS_WIRE_GUARD_OBFUSCATION_GENERAL", + "VPN_SETTINGS_WIREGUARD_OBFUSCATION_GENERAL", tableName: "WireGuardObfuscation", value: """ Obfuscation hides the WireGuard traffic inside another protocol. \ @@ -69,7 +69,7 @@ enum VPNSettingsInfoButtonItem: CustomStringConvertible { ) case .wireGuardObfuscationPort: NSLocalizedString( - "VPN_SETTINGS_WIRE_GUARD_OBFUSCATION_PORT_GENERAL", + "VPN_SETTINGS_WIREGUARD_OBFUSCATION_PORT_GENERAL", tableName: "WireGuardObfuscation", value: "Which TCP port the UDP-over-TCP obfuscation protocol should connect to on the VPN server.", comment: "" diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift index c86db1b9d09b..10a519a737af 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift @@ -111,6 +111,11 @@ extension VPNSettingsViewController: VPNSettingsDataSourceDelegate { alertPresenter.showAlert(presentation: presentation, animated: true) } + func showDetails(for: VPNSettingsDetailsButtonItem) { + // TODO: When ready, add navigation to detail views for selecting obfuscation options for + // UDP-over-TCP and shadowsocks. + } + func showDNSSettings() { let viewController = CustomDNSViewController(interactor: interactor, alertPresenter: alertPresenter) navigationController?.pushViewController(viewController, animated: true) diff --git a/ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift b/ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift index 2b2f8bcfe45a..570f32ab080f 100644 --- a/ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift +++ b/ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift @@ -103,7 +103,7 @@ class VPNSettingsPage: Page { } @discardableResult func tapWireGuardObfuscationOnCell() -> Self { - app.cells[AccessibilityIdentifier.wireGuardObfuscationOn].tap() + app.cells[AccessibilityIdentifier.wireGuardObfuscationUdpOverTcp].tap() return self } @@ -157,7 +157,7 @@ class VPNSettingsPage: Page { } @discardableResult func verifyWireGuardObfuscationOnSelected() -> Self { - let onCell = app.cells[AccessibilityIdentifier.wireGuardObfuscationOn] + let onCell = app.cells[AccessibilityIdentifier.wireGuardObfuscationUdpOverTcp] XCTAssertTrue(onCell.isSelected) return self }