diff options
25 files changed, 526 insertions, 305 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 9fed881999..af02794d84 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -235,7 +235,7 @@ 588527B4276B4F2F00BAA373 /* SetAccountOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588527B3276B4F2F00BAA373 /* SetAccountOperation.swift */; }; 5888AD83227B11080051EB06 /* LocationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD82227B11080051EB06 /* LocationCell.swift */; }; 5888AD87227B17950051EB06 /* LocationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD86227B17950051EB06 /* LocationViewController.swift */; }; - 588D7ED62AF3903F005DF40A /* ListAccessMethodViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588D7ED52AF3903F005DF40A /* ListAccessMethodViewController.swift */; }; + 588D7ED62AF3903F005DF40A /* ListAccessMethodView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588D7ED52AF3903F005DF40A /* ListAccessMethodView.swift */; }; 588D7EDC2AF3A55E005DF40A /* ListAccessMethodInteractorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588D7EDB2AF3A55E005DF40A /* ListAccessMethodInteractorProtocol.swift */; }; 588D7EDE2AF3A585005DF40A /* ListAccessMethodItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588D7EDD2AF3A585005DF40A /* ListAccessMethodItem.swift */; }; 588D7EE02AF3A595005DF40A /* ListAccessMethodInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588D7EDF2AF3A595005DF40A /* ListAccessMethodInteractor.swift */; }; @@ -1108,12 +1108,18 @@ F910A4312D4A1B41002FF3BB /* InAppPurchaseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F910A4302D4A1B3B002FF3BB /* InAppPurchaseCoordinator.swift */; }; F910A43A2D4A283D002FF3BB /* InAppPurchaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F910A4392D4A2839002FF3BB /* InAppPurchaseViewController.swift */; }; F910A8572D523812002FF3BB /* TunnelSettingsV7.swift in Sources */ = {isa = PBXBuildFile; fileRef = F910A8562D523812002FF3BB /* TunnelSettingsV7.swift */; }; - F924C5A42DA65F28001F4660 /* Storekit2.swift in Sources */ = {isa = PBXBuildFile; fileRef = F924C5A32DA65F28001F4660 /* Storekit2.swift */; }; - F924C65F2DAE4554001F4660 /* ServerRelayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F924C65E2DAE4554001F4660 /* ServerRelayTests.swift */; }; + F91B94A72DC9EB5E00132C28 /* MullvadInfoHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F91B94A62DC9EB5E00132C28 /* MullvadInfoHeaderView.swift */; }; F924C4532D70692E001F4660 /* MullvadApiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F924C4522D706929001F4660 /* MullvadApiTests.swift */; }; + F924C5A42DA65F28001F4660 /* Storekit2.swift in Sources */ = {isa = PBXBuildFile; fileRef = F924C5A32DA65F28001F4660 /* Storekit2.swift */; }; F924C65F2DAE4554001F4660 /* ServerRelayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F924C65E2DAE4554001F4660 /* ServerRelayTests.swift */; }; + F9276C622DBA2103006FE43D /* Font+Mullvad.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9276C612DBA20FC006FE43D /* Font+Mullvad.swift */; }; + F9394EEC2DBF56B6009595EA /* Color+Mullvad.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9394EEB2DBF56AA009595EA /* Color+Mullvad.swift */; }; + F9394EED2DBF56B6009595EA /* Color+Mullvad.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9394EEB2DBF56AA009595EA /* Color+Mullvad.swift */; }; + F9394EF02DC0B58D009595EA /* MullvadListNavigationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9394EEF2DC0B58D009595EA /* MullvadListNavigationItemView.swift */; }; + F9394EF32DC21D8C009595EA /* MullvadList.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9394EF22DC21D8C009595EA /* MullvadList.swift */; }; F998EFF82D359C4600D88D01 /* SKProduct+Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */; }; F998EFFA2D3656BA00D88D01 /* SKProduct+Sorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = F998EFF92D3656B100D88D01 /* SKProduct+Sorting.swift */; }; + F9E3BCF72DD35B78009986C3 /* ListAccessViewModelBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E3BCF62DD35B78009986C3 /* ListAccessViewModelBridge.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1849,7 +1855,7 @@ 588527B3276B4F2F00BAA373 /* SetAccountOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetAccountOperation.swift; sourceTree = "<group>"; }; 5888AD82227B11080051EB06 /* LocationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationCell.swift; sourceTree = "<group>"; }; 5888AD86227B17950051EB06 /* LocationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationViewController.swift; sourceTree = "<group>"; }; - 588D7ED52AF3903F005DF40A /* ListAccessMethodViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListAccessMethodViewController.swift; sourceTree = "<group>"; }; + 588D7ED52AF3903F005DF40A /* ListAccessMethodView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListAccessMethodView.swift; sourceTree = "<group>"; }; 588D7ED72AF3A533005DF40A /* AccessMethodKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodKind.swift; sourceTree = "<group>"; }; 588D7EDB2AF3A55E005DF40A /* ListAccessMethodInteractorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListAccessMethodInteractorProtocol.swift; sourceTree = "<group>"; }; 588D7EDD2AF3A585005DF40A /* ListAccessMethodItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListAccessMethodItem.swift; sourceTree = "<group>"; }; @@ -2523,11 +2529,16 @@ F910A4302D4A1B3B002FF3BB /* InAppPurchaseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseCoordinator.swift; sourceTree = "<group>"; }; F910A4392D4A2839002FF3BB /* InAppPurchaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseViewController.swift; sourceTree = "<group>"; }; F910A8562D523812002FF3BB /* TunnelSettingsV7.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV7.swift; sourceTree = "<group>"; }; - F924C65E2DAE4554001F4660 /* ServerRelayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerRelayTests.swift; sourceTree = "<group>"; }; - F924C5A32DA65F28001F4660 /* Storekit2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storekit2.swift; sourceTree = "<group>"; }; + F91B94A62DC9EB5E00132C28 /* MullvadInfoHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadInfoHeaderView.swift; sourceTree = "<group>"; }; F924C4522D706929001F4660 /* MullvadApiTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiTests.swift; sourceTree = "<group>"; }; + F924C5A32DA65F28001F4660 /* Storekit2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storekit2.swift; sourceTree = "<group>"; }; F924C65E2DAE4554001F4660 /* ServerRelayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerRelayTests.swift; sourceTree = "<group>"; }; + F9276C612DBA20FC006FE43D /* Font+Mullvad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Font+Mullvad.swift"; sourceTree = "<group>"; }; + F9394EEB2DBF56AA009595EA /* Color+Mullvad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Mullvad.swift"; sourceTree = "<group>"; }; + F9394EEF2DC0B58D009595EA /* MullvadListNavigationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadListNavigationItemView.swift; sourceTree = "<group>"; }; + F9394EF22DC21D8C009595EA /* MullvadList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadList.swift; sourceTree = "<group>"; }; F998EFF92D3656B100D88D01 /* SKProduct+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKProduct+Sorting.swift"; sourceTree = "<group>"; }; + F9E3BCF62DD35B78009986C3 /* ListAccessViewModelBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListAccessViewModelBridge.swift; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -3315,8 +3326,10 @@ F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */, 7A9F29382CABFAEC005F2089 /* InfoHeaderView.swift */, 7A5869942B32E9C700640D27 /* LinkButton.swift */, + F9394EF12DC21D7B009595EA /* List */, 7AA1309A2D0048D800640DF9 /* MainButton.swift */, 7A0EAE992D01B41500D3EB8B /* MainButtonStyle.swift */, + F91B94A62DC9EB5E00132C28 /* MullvadInfoHeaderView.swift */, 7A8A190F2CEE3918000BCB5B /* RowSeparator.swift */, 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */, 7AA130A02D01B1E200640DF9 /* SplitMainButton.swift */, @@ -3431,6 +3444,8 @@ 583FE02729C1ADF7006E85F9 /* UI appearance */ = { isa = PBXGroup; children = ( + F9394EEB2DBF56AA009595EA /* Color+Mullvad.swift */, + F9276C612DBA20FC006FE43D /* Font+Mullvad.swift */, 58CCA0152242560B004F3011 /* UIColor+Palette.swift */, A9E034632ABB302000E59A5A /* UIEdgeInsets+Extensions.swift */, 585CA70E25F8C44600B47C62 /* UIMetrics.swift */, @@ -3992,11 +4007,12 @@ 58CEB2EA2AFBBCBA00E6E088 /* List */ = { isa = PBXGroup; children = ( + F9E3BCF62DD35B78009986C3 /* ListAccessViewModelBridge.swift */, 58EFC76D2AFB3BDA00E9F4CB /* ListAccessMethodCoordinator.swift */, 588D7EDF2AF3A595005DF40A /* ListAccessMethodInteractor.swift */, 588D7EDB2AF3A55E005DF40A /* ListAccessMethodInteractorProtocol.swift */, 588D7EDD2AF3A585005DF40A /* ListAccessMethodItem.swift */, - 588D7ED52AF3903F005DF40A /* ListAccessMethodViewController.swift */, + 588D7ED52AF3903F005DF40A /* ListAccessMethodView.swift */, 5827B0AF2B0F4CCD00CCBBA1 /* ListAccessMethodViewControllerDelegate.swift */, ); path = List; @@ -4881,6 +4897,15 @@ path = InAppPurchase; sourceTree = "<group>"; }; + F9394EF12DC21D7B009595EA /* List */ = { + isa = PBXGroup; + children = ( + F9394EEF2DC0B58D009595EA /* MullvadListNavigationItemView.swift */, + F9394EF22DC21D8C009595EA /* MullvadList.swift */, + ); + path = List; + sourceTree = "<group>"; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -5910,6 +5935,7 @@ 7A5869C32B5820CE00640D27 /* IPOverrideRepositoryTests.swift in Sources */, A9A5FA392ACB05910083449F /* UIColor+Palette.swift in Sources */, 7A5468AD2C6B5E4B00590086 /* LocationRelays.swift in Sources */, + F9394EEC2DBF56B6009595EA /* Color+Mullvad.swift in Sources */, A9A5FA3A2ACB05910083449F /* UIEdgeInsets+Extensions.swift in Sources */, A9A5FA3B2ACB05910083449F /* UIMetrics.swift in Sources */, 58B07C182AEFDD6C00A09625 /* StoreTransactionLog.swift in Sources */, @@ -6217,6 +6243,7 @@ buildActionMask = 2147483647; files = ( 44075DFB2CDA4F7400F61139 /* UDPOverTCPObfuscationSettingsViewModel.swift in Sources */, + F9E3BCF72DD35B78009986C3 /* ListAccessViewModelBridge.swift in Sources */, 7A6389DC2B7E3BD6008E77E1 /* CustomListViewModel.swift in Sources */, 4422C0712CCFF6790001A385 /* UDPOverTCPObfuscationSettingsView.swift in Sources */, 7A95B67D2D5F7C5B00687524 /* DAITASettingsCoordinator.swift in Sources */, @@ -6321,7 +6348,7 @@ E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */, 7AC8A3AE2ABC6FBB00DC4939 /* SettingsHeaderView.swift in Sources */, 588D7EDC2AF3A55E005DF40A /* ListAccessMethodInteractorProtocol.swift in Sources */, - 588D7ED62AF3903F005DF40A /* ListAccessMethodViewController.swift in Sources */, + 588D7ED62AF3903F005DF40A /* ListAccessMethodView.swift in Sources */, 7A8A190E2CEB77C1000BCB5B /* SettingsRowViewFooter.swift in Sources */, 7A6000FC2B628DF6001CF0D9 /* ListCellContentConfiguration.swift in Sources */, 582BB1B1229569620055B6EF /* UINavigationBar+Appearance.swift in Sources */, @@ -6348,6 +6375,7 @@ 581DFAEC2B1770C1005D6D1C /* AccessMethodViewModel+NavigationItem.swift in Sources */, 58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */, F0DA874B2A9CBACB006044F1 /* AccountNumberRow.swift in Sources */, + F91B94A72DC9EB5E00132C28 /* MullvadInfoHeaderView.swift in Sources */, 7A5869C72B5A8E4C00640D27 /* MethodSettingsDataSourceConfiguration.swift in Sources */, 58F2E14C276A61C000A79513 /* RotateKeyOperation.swift in Sources */, 5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */, @@ -6371,6 +6399,7 @@ 7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */, 7A9CCCBF2A96302800DD6A34 /* SettingsCoordinator.swift in Sources */, 58F70FE52AEA707800E6890E /* StoreTransactionLog.swift in Sources */, + F9394EF02DC0B58D009595EA /* MullvadListNavigationItemView.swift in Sources */, 582AE3102440A6CA00E6733A /* InputTextFormatter.swift in Sources */, 7A6F2FAD2AFD3DA7006D0856 /* CustomDNSViewController.swift in Sources */, 5820EDAB288FF0D2006BF4E4 /* DeviceRowView.swift in Sources */, @@ -6432,6 +6461,7 @@ 5820676426E771DB00655B05 /* TunnelManagerErrors.swift in Sources */, 585B4B8726D9098900555C4C /* TunnelStatusNotificationProvider.swift in Sources */, 7AF9BE972A41C71F00DBFEDB /* ChipViewCell.swift in Sources */, + F9276C622DBA2103006FE43D /* Font+Mullvad.swift in Sources */, 063F026628FFE11C001FA09F /* RESTCreateApplePaymentResponse+Localization.swift in Sources */, 58DF28A52417CB4B00E836B0 /* StorePaymentManager.swift in Sources */, 583DA21425FA4B5C00318683 /* LocationDataSource.swift in Sources */, @@ -6497,6 +6527,7 @@ F910A43A2D4A283D002FF3BB /* InAppPurchaseViewController.swift in Sources */, 58607A4D2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift in Sources */, 7A8A18FD2CE4BE8D000BCB5B /* CustomToggleStyle.swift in Sources */, + F9394EF32DC21D8C009595EA /* MullvadList.swift in Sources */, 58C8191829FAA2C400DEB1B4 /* NotificationConfiguration.swift in Sources */, 58FF9FE82B07650A00E4C97D /* ButtonCellContentConfiguration.swift in Sources */, 7A6652B82BB44C3E0042D848 /* LocationDiffableDataSourceProtocol.swift in Sources */, @@ -6508,6 +6539,7 @@ 586C0D832B03D2FF00E7CDD7 /* ShadowsocksSectionHandler.swift in Sources */, 58B26E262943522400D5980C /* NotificationProvider.swift in Sources */, 58CE5E64224146200008646E /* AppDelegate.swift in Sources */, + F9394EED2DBF56B6009595EA /* Color+Mullvad.swift in Sources */, F0DA87492A9CBA9F006044F1 /* AccountDeviceRow.swift in Sources */, 58FF9FE42B075BDD00E4C97D /* EditAccessMethodItemIdentifier.swift in Sources */, 449E9A6F2D283C7400F8574A /* ButtonPanel.swift in Sources */, diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift index 82d9d058cc..63f3c87682 100644 --- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift +++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift @@ -126,6 +126,7 @@ public enum AccessibilityIdentifier: Equatable { case addLocationsView case addAccessMethodTableView case apiAccessView + case apiAccessListView case alertContainerView case alertTitle case appLogsView diff --git a/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift index 368422c360..d6cf5e7ac4 100644 --- a/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift +++ b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift @@ -25,7 +25,7 @@ class AddLocationsViewController: UIViewController { let tableView = UITableView() tableView.separatorColor = .secondaryColor tableView.separatorInset = .zero - tableView.rowHeight = 56 + tableView.rowHeight = UIMetrics.TableView.rowHeight tableView.indicatorStyle = .white tableView.setAccessibilityIdentifier(.editCustomListEditLocationsTableView) return tableView diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodCoordinator.swift index d827b1f29b..804f9b0e34 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodCoordinator.swift @@ -8,6 +8,7 @@ import MullvadSettings import Routing +import SwiftUI import UIKit class ListAccessMethodCoordinator: Coordinator, Presenting, SettingsChildCoordinator { @@ -30,11 +31,21 @@ class ListAccessMethodCoordinator: Coordinator, Presenting, SettingsChildCoordin } func start(animated: Bool) { - let listController = ListAccessMethodViewController( - interactor: ListAccessMethodInteractor(repository: accessMethodRepository) + let view = ListAccessMethodView( + viewModel: ListAccessViewModelBridge(interactor: ListAccessMethodInteractor( + repository: accessMethodRepository + ), delegate: self) ) - listController.delegate = self - navigationController.pushViewController(listController, animated: animated) + let host = UIHostingController(rootView: view) + host.title = NSLocalizedString( + "NAVIGATION_TITLE", + tableName: "Settings", + value: "API access", + comment: "" + ) + host.view.setAccessibilityIdentifier(.apiAccessView) + + navigationController.pushViewController(host, animated: animated) } private func addNew() { @@ -68,7 +79,9 @@ class ListAccessMethodCoordinator: Coordinator, Presenting, SettingsChildCoordin private func popToList() { guard let listController = navigationController.viewControllers - .first(where: { $0 is ListAccessMethodViewController }) else { return } + .first(where: { $0 is UIHostingController<ListAccessMethodView<ListAccessViewModelBridge>> }) else { + return + } navigationController.popToViewController(listController, animated: true) } @@ -131,15 +144,15 @@ class ListAccessMethodCoordinator: Coordinator, Presenting, SettingsChildCoordin } extension ListAccessMethodCoordinator: @preconcurrency ListAccessMethodViewControllerDelegate { - func controllerShouldShowAbout(_ controller: ListAccessMethodViewController) { + func controllerShouldShowAbout() { about() } - func controllerShouldAddNew(_ controller: ListAccessMethodViewController) { + func controllerShouldAddNew() { addNew() } - func controller(_ controller: ListAccessMethodViewController, shouldEditItem item: ListAccessMethodItem) { + func controller(shouldEditItem item: ListAccessMethodItem) { edit(item: item) } } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractor.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractor.swift index e262f7bcab..bfbf73fb4b 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractor.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractor.swift @@ -51,14 +51,7 @@ extension PersistentAccessMethod { return ListAccessMethodItem( id: id, name: itemName, - detail: isEnabled - ? kind.localizedDescription - : NSLocalizedString( - "LIST_ACCESS_METHODS_DISABLED", - tableName: "APIAccess", - value: "Disabled", - comment: "" - ), + detail: kind.localizedDescription, isEnabled: isEnabled ) } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodView.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodView.swift new file mode 100644 index 0000000000..8c06bea36e --- /dev/null +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodView.swift @@ -0,0 +1,125 @@ +// +// ListAccessMethodViewController.swift +// MullvadVPN +// +// Created by pronebird on 02/11/2023. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import Combine +import MullvadREST +import MullvadSettings +import SwiftUI + +protocol ListAccessViewModel: ObservableObject { + var items: [ListAccessMethodItem] { get } + var itemInUse: ListAccessMethodItem? { get } + func addNewMethod() + func methodSelected(_ method: ListAccessMethodItem) + func showAbout() +} + +struct ListAccessMethodView<ViewModel>: View where ViewModel: ListAccessViewModel { + @ObservedObject var viewModel: ViewModel + + init(viewModel: ViewModel) { + self.viewModel = viewModel + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + let text = NSLocalizedString( + "ACCESS_METHOD_HEADER_BODY", + tableName: "APIAccess", + value: "Manage default and setup custom methods to access the Mullvad API. ", + comment: "" + ) + let about = NSLocalizedString( + "ACCESS_METHOD_HEADER_BODY", + tableName: "APIAccess", + value: "About API access…", + comment: "" + ) + MullvadInfoHeaderView( + bodyText: text, + link: about, + onTapLink: viewModel.showAbout + ) + .padding(.horizontal, 16) + .padding(.bottom, 16) + MullvadList(viewModel.items) { item in + let accessibilityId: AccessibilityIdentifier? = switch item.id { + case AccessMethodRepository.directId: + AccessibilityIdentifier.accessMethodDirectCell + case AccessMethodRepository.bridgeId: + AccessibilityIdentifier.accessMethodBridgesCell + case AccessMethodRepository.encryptedDNSId: + AccessibilityIdentifier.accessMethodEncryptedDNSCell + default: + nil + } + let state = viewModel.itemInUse?.id == item.id + ? NSLocalizedString( + "LIST_ACCESS_METHODS_IN_USE_ITEM", + tableName: "APIAccess", + value: "In use", + comment: "" + ) + : ( + !item.isEnabled + ? NSLocalizedString( + "LIST_ACCESS_METHODS_DISABLED", + tableName: "APIAccess", + value: "Disabled", + comment: "" + ) + : nil + ) + MullvadListNavigationItemView( + item: MullvadListNavigationItem( + id: item.id, + title: item.name, + state: state, + detail: item.detail, + accessibilityIdentifier: accessibilityId + ) { + viewModel.methodSelected(item) + } + ) + } + .accessibilityIdentifier( + AccessibilityIdentifier.apiAccessListView.asString + ) + .apply { + if #available(iOS 16.4, *) { + $0.scrollBounceBehavior(.basedOnSize) + } else { + $0 + } + } + .padding(.bottom, 24) + MainButton( + text: LocalizedStringKey("Add"), + style: .default + ) { + viewModel.addNewMethod() + } + .padding(.horizontal) + Spacer() + } + .background(Color.mullvadBackground) + } +} + +#Preview { + NavigationView { + ListAccessMethodView( + viewModel: ListAccessViewModelBridge( + interactor: ListAccessMethodInteractor( + repository: AccessMethodRepository() + ), delegate: nil + ) + ) + .navigationTitle("API Access") + } +} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift deleted file mode 100644 index d04a532f1c..0000000000 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift +++ /dev/null @@ -1,259 +0,0 @@ -// -// ListAccessMethodViewController.swift -// MullvadVPN -// -// Created by pronebird on 02/11/2023. -// Copyright © 2025 Mullvad VPN AB. All rights reserved. -// - -import Combine -import MullvadREST -import MullvadSettings -import UIKit - -enum ListAccessMethodSectionIdentifier: Hashable { - case primary -} - -struct ListAccessMethodItemIdentifier: Hashable { - let id: UUID -} - -/// View controller presenting a list of API access methods. -class ListAccessMethodViewController: UIViewController, UITableViewDelegate { - typealias ListAccessMethodDataSource = UITableViewDiffableDataSource< - ListAccessMethodSectionIdentifier, - ListAccessMethodItemIdentifier - > - - private let interactor: ListAccessMethodInteractorProtocol - private var lastReachableMethodItem: ListAccessMethodItem? - private var cancellables = Set<AnyCancellable>() - - private var dataSource: ListAccessMethodDataSource? - private var fetchedItems: [ListAccessMethodItem] = [] - private let contentController = UITableViewController(style: .plain) - private var tableView: UITableView { - contentController.tableView - } - - weak var delegate: ListAccessMethodViewControllerDelegate? - - /// Designated initializer. - /// - Parameter interactor: the object implementing access and manipulation of the API access list. - init(interactor: ListAccessMethodInteractorProtocol) { - self.interactor = interactor - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .secondaryColor - - tableView.delegate = self - tableView.backgroundColor = .secondaryColor - tableView.separatorColor = .secondaryColor - tableView.separatorInset = .zero - - tableView.registerReusableViews(from: CellReuseIdentifier.self) - - view.setAccessibilityIdentifier(.apiAccessView) - - let headerView = createHeaderView() - view.addConstrainedSubviews([headerView, tableView]) { - headerView.pinEdgesToSuperviewMargins(PinnableEdges([.leading(8), .trailing(8), .top(0)])) - tableView.pinEdgesToSuperview(.all().excluding(.top)) - tableView.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 20) - } - - addChild(contentController) - contentController.didMove(toParent: self) - - interactor.itemsPublisher.sink { [weak self] _ in - self?.updateDataSource(animated: true) - } - .store(in: &cancellables) - - interactor.itemInUsePublisher.sink { [weak self] item in - self?.lastReachableMethodItem = item - self?.updateDataSource(animated: true) - } - .store(in: &cancellables) - - configureNavigationItem() - configureDataSource() - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - guard let itemIdentifier = dataSource?.itemIdentifier(for: indexPath) else { return 0 } - - if itemIdentifier.id == lastReachableMethodItem?.id { - return UITableView.automaticDimension - } else { - return UIMetrics.SettingsCell.apiAccessCellHeight - } - } - - func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - let container = UIView() - - let button = AppButton(style: .tableInsetGroupedDefault) - button.setTitle( - NSLocalizedString( - "LIST_ACCESS_METHODS_ADD_BUTTON", - tableName: "APIAccess", - value: "Add", - comment: "" - ), - for: .normal - ) - button.addAction(UIAction { [weak self] _ in - self?.sendAddNew() - }, for: .touchUpInside) - button.setAccessibilityIdentifier(.addAccessMethodButton) - - let fontSize = button.titleLabel?.font.pointSize ?? 0 - button.titleLabel?.font = UIFont.systemFont(ofSize: fontSize, weight: .regular) - - container.addConstrainedSubviews([button]) { - button.pinEdgesToSuperview(.init([.top(40), .trailing(16), .bottom(0), .leading(16)])) - } - - container.directionalLayoutMargins = UIMetrics.SettingsCell.apiAccessInsetLayoutMargins - - return container - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let item = fetchedItems[indexPath.row] - sendEdit(item: item) - } - - private func configureNavigationItem() { - navigationItem.title = NSLocalizedString( - "NAVIGATION_TITLE", - tableName: "Settings", - value: "API access", - comment: "" - ) - } - - private func createHeaderView() -> InfoHeaderView { - let body = NSLocalizedString( - "ACCESS_METHOD_HEADER_BODY", - tableName: "APIAccess", - value: "Manage default and setup custom methods to access the Mullvad API.", - comment: "" - ) - let link = NSLocalizedString( - "ACCESS_METHOD_HEADER_LINK", - tableName: "APIAccess", - value: "About API access...", - comment: "" - ) - - let headerView = InfoHeaderView(config: InfoHeaderConfig(body: body, link: link)) - - headerView.onAbout = { [weak self] in - self?.sendAbout() - } - - return headerView - } - - private func configureDataSource() { - dataSource = ListAccessMethodDataSource( - tableView: tableView, - cellProvider: { [weak self] _, indexPath, itemIdentifier in - self?.dequeueCell(at: indexPath, itemIdentifier: itemIdentifier) - } - ) - updateDataSource(animated: false) - } - - private func updateDataSource(animated: Bool = true) { - guard let dataSource else { return } - fetchedItems = interactor.fetch() - - var snapshot = NSDiffableDataSourceSnapshot<ListAccessMethodSectionIdentifier, ListAccessMethodItemIdentifier>() - snapshot.appendSections([.primary]) - - let itemIdentifiers = fetchedItems.map { item in - ListAccessMethodItemIdentifier(id: item.id) - } - snapshot.appendItems(itemIdentifiers, toSection: .primary) - - if dataSource.snapshot().numberOfItems == fetchedItems.count { - for item in fetchedItems { - snapshot.reloadItems([ListAccessMethodItemIdentifier(id: item.id)]) - } - } - - dataSource.apply(snapshot, animatingDifferences: animated) - } - - private func dequeueCell( - at indexPath: IndexPath, - itemIdentifier: ListAccessMethodItemIdentifier - ) -> UITableViewCell { - let cell = tableView.dequeueReusableView(withIdentifier: CellReuseIdentifier.default, for: indexPath) - let item = fetchedItems[indexPath.row] - - var contentConfiguration = ListCellContentConfiguration() - contentConfiguration.text = item.name - contentConfiguration.secondaryText = item.detail - contentConfiguration.tertiaryText = lastReachableMethodItem?.id == item.id - ? NSLocalizedString("LIST_ACCESS_METHODS_IN_USE_ITEM", tableName: "APIAccess", value: "In use", comment: "") - : "" - cell.contentConfiguration = contentConfiguration - - if let cell = cell as? DynamicBackgroundConfiguration { - cell.setAutoAdaptingBackgroundConfiguration(.mullvadListPlainCell(), selectionType: .dimmed) - } - - if let cell = cell as? CustomCellDisclosureHandling { - cell.disclosureType = .chevron - } - - let accessibilityId: AccessibilityIdentifier? = switch item.id.uuidString { - case AccessMethodRepository.directId.uuidString: - AccessibilityIdentifier.accessMethodDirectCell - case AccessMethodRepository.bridgeId.uuidString: - AccessibilityIdentifier.accessMethodBridgesCell - case AccessMethodRepository.encryptedDNSId.uuidString: - AccessibilityIdentifier.accessMethodEncryptedDNSCell - default: - nil - } - cell.setAccessibilityIdentifier(accessibilityId) - - return cell - } - - private func sendAddNew() { - delegate?.controllerShouldAddNew(self) - } - - private func sendAbout() { - delegate?.controllerShouldShowAbout(self) - } - - private func sendEdit(item: ListAccessMethodItem) { - delegate?.controller(self, shouldEditItem: item) - } -} - -private enum CellReuseIdentifier: String, CaseIterable, CellIdentifierProtocol { - case `default` - - var cellClass: AnyClass { - switch self { - case .default: BasicCell.self - } - } -} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewControllerDelegate.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewControllerDelegate.swift index 26763c5135..f3d5185d92 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewControllerDelegate.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewControllerDelegate.swift @@ -12,17 +12,17 @@ protocol ListAccessMethodViewControllerDelegate: AnyObject { /// The view controller requests the delegate to present the about view. /// /// - Parameter controller: the calling view controller. - func controllerShouldShowAbout(_ controller: ListAccessMethodViewController) + func controllerShouldShowAbout() /// The view controller requests the delegate to present the add new method controller. /// /// - Parameter controller: the calling view controller. - func controllerShouldAddNew(_ controller: ListAccessMethodViewController) + func controllerShouldAddNew() /// The view controller requests the delegate to present the view controller for editing the existing access method. /// /// - Parameters: /// - controller: the calling view controller /// - item: the selected item. - func controller(_ controller: ListAccessMethodViewController, shouldEditItem item: ListAccessMethodItem) + func controller(shouldEditItem item: ListAccessMethodItem) } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessViewModelBridge.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessViewModelBridge.swift new file mode 100644 index 0000000000..a6f1e889e3 --- /dev/null +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessViewModelBridge.swift @@ -0,0 +1,30 @@ +import SwiftUI + +class ListAccessViewModelBridge: ListAccessViewModel { + private let interactor: ListAccessMethodInteractorProtocol + private weak var delegate: ListAccessMethodViewControllerDelegate? + + @Published var items: [ListAccessMethodItem] = [] + @Published var itemInUse: ListAccessMethodItem? + init( + interactor: ListAccessMethodInteractorProtocol, + delegate: ListAccessMethodViewControllerDelegate? + ) { + self.interactor = interactor + self.delegate = delegate + interactor.itemsPublisher.assign(to: &$items) + interactor.itemInUsePublisher.assign(to: &$itemInUse) + } + + func addNewMethod() { + delegate?.controllerShouldAddNew() + } + + func methodSelected(_ method: ListAccessMethodItem) { + delegate?.controller(shouldEditItem: method) + } + + func showAbout() { + delegate?.controllerShouldShowAbout() + } +} diff --git a/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift index 52a0b36b0f..dae1b67605 100644 --- a/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift @@ -268,7 +268,7 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV return .vpnSettings case is ProblemReportViewController: return .problemReport - case is ListAccessMethodViewController: + case is UIHostingController<ListAccessMethodView<ListAccessViewModelBridge>>: return .apiAccess default: return nil diff --git a/ios/MullvadVPN/UI appearance/Color+Mullvad.swift b/ios/MullvadVPN/UI appearance/Color+Mullvad.swift new file mode 100644 index 0000000000..5d6cd0da42 --- /dev/null +++ b/ios/MullvadVPN/UI appearance/Color+Mullvad.swift @@ -0,0 +1,31 @@ +import SwiftUI + +extension Color { + private static let mullvadPrimaryColor = UIColor.primaryColor.color + private static let mullvadSecondaryColor = UIColor.secondaryColor.color + private static let mullvadWarningColor = UIColor.warningColor.color + private static let mullvadDangerColor = UIColor.dangerColor.color + private static let mullvadSuccessColor = UIColor.successColor.color + + static let mullvadBackground: Color = .mullvadSecondaryColor + static let mullvadTextPrimary: Color = UIColor.primaryTextColor.color + static let mullvadTextPrimaryDisabled: Color = .mullvadPrimaryColor.opacity( + 0.2 + ) + + enum MullvadButton { + static let primary: Color = .mullvadPrimaryColor + static let primaryPressed = Color(red: 0.12, green: 0.23, blue: 0.34) + static let primaryDisabled = primaryPressed + static let danger: Color = .mullvadDangerColor + static let dangerPressed = Color(red: 0.42, green: 0.21, blue: 0.25) + static let dangerDisabled = dangerPressed + static let positive: Color = .mullvadSuccessColor + static let positivePressed = Color(red: 0.16, green: 0.38, blue: 0.28) + static let positiveDisabled = positivePressed + } + + enum MullvadList { + static let separator: Color = .mullvadSecondaryColor + } +} diff --git a/ios/MullvadVPN/UI appearance/Font+Mullvad.swift b/ios/MullvadVPN/UI appearance/Font+Mullvad.swift new file mode 100644 index 0000000000..9a77d6d690 --- /dev/null +++ b/ios/MullvadVPN/UI appearance/Font+Mullvad.swift @@ -0,0 +1,25 @@ +import SwiftUI + +extension Font { + static let mullvadBig: Font = .largeTitle.bold() + static let mullvadLarge: Font = .title.bold() + static let mullvadMedium: Font = .title3.weight(.semibold) + static let mullvadSmall: Font = .body + static let mullvadSmallSemiBold: Font = mullvadSmall.weight(.semibold) + static let mullvadTiny: Font = .subheadline + static let mullvadTinySemiBold: Font = .mullvadTiny.weight(.semibold) + static let mullvadMini: Font = .footnote + static let mullvadMiniSemiBold: Font = mullvadMini.weight(.semibold) +} + +extension UIFont { + static let mullvadBig: UIFont = .preferredFont(forTextStyle: .largeTitle, weight: .bold) + static let mullvadLarge: UIFont = .preferredFont(forTextStyle: .title1, weight: .bold) + static let mullvadMedium: UIFont = .preferredFont(forTextStyle: .title3, weight: .semibold) + static let mullvadSmall: UIFont = .preferredFont(forTextStyle: .body) + static let mullvadSmallSemiBold: UIFont = .preferredFont(forTextStyle: .body, weight: .semibold) + static let mullvadTiny: UIFont = .preferredFont(forTextStyle: .subheadline) + static let mullvadTinySemiBold: UIFont = .preferredFont(forTextStyle: .subheadline, weight: .semibold) + static let mullvadMini: UIFont = .preferredFont(forTextStyle: .footnote) + static let mullvadMiniSemiBold: UIFont = .preferredFont(forTextStyle: .footnote, weight: .semibold) +} diff --git a/ios/MullvadVPN/UI appearance/UIColor+Palette.swift b/ios/MullvadVPN/UI appearance/UIColor+Palette.swift index 14ed92c90f..faee4c4ce0 100644 --- a/ios/MullvadVPN/UI appearance/UIColor+Palette.swift +++ b/ios/MullvadVPN/UI appearance/UIColor+Palette.swift @@ -95,7 +95,7 @@ extension UIColor { static let indentationLevelTwo = UIColor(red: 0.11, green: 0.20, blue: 0.31, alpha: 1.0) static let indentationLevelThree = UIColor(red: 0.11, green: 0.19, blue: 0.29, alpha: 1.0) - static let normal = indentationLevelZero + static let normal = UIColor.primaryColor static let disabled = normal.darkened(by: 0.1)! static let selected = successColor static let disabledSelected = selected.darkened(by: 0.3)! diff --git a/ios/MullvadVPN/UI appearance/UIMetrics.swift b/ios/MullvadVPN/UI appearance/UIMetrics.swift index 4f4c1142cc..47e83662dc 100644 --- a/ios/MullvadVPN/UI appearance/UIMetrics.swift +++ b/ios/MullvadVPN/UI appearance/UIMetrics.swift @@ -12,6 +12,8 @@ import UIKit enum UIMetrics { enum TableView { + /// Height of a cell. + static let rowHeight: CGFloat = 56 /// Height for separators between cells and/or sections. static let separatorHeight: CGFloat = 0.33 /// Spacing used between distinct sections of views diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationCell.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationCell.swift index e3cbedaf05..ba1958998c 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationCell.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationCell.swift @@ -252,7 +252,7 @@ class LocationCell: UITableViewCell { case 3: return UIColor.Cell.Background.indentationLevelThree default: - return UIColor.Cell.Background.normal + return UIColor.Cell.Background.indentationLevelZero } } diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift index bdae59b546..8f1336ff21 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift @@ -171,8 +171,8 @@ final class LocationViewController: UIViewController { tableView.backgroundColor = view.backgroundColor tableView.separatorColor = .secondaryColor tableView.separatorInset = .zero - tableView.rowHeight = 56 - tableView.sectionHeaderHeight = 56 + tableView.rowHeight = UIMetrics.TableView.rowHeight + tableView.sectionHeaderHeight = UIMetrics.TableView.rowHeight tableView.indicatorStyle = .white tableView.keyboardDismissMode = .onDrag tableView.setAccessibilityIdentifier(.selectLocationTableView) diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsAddDNSEntryCell.swift b/ios/MullvadVPN/View controllers/Settings/SettingsAddDNSEntryCell.swift index 3becd35d1f..c144967637 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsAddDNSEntryCell.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsAddDNSEntryCell.swift @@ -14,7 +14,7 @@ class SettingsAddDNSEntryCell: SettingsCell { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - backgroundView?.backgroundColor = UIColor.Cell.Background.indentationLevelTwo + backgroundView?.backgroundColor = UIColor.Cell.Background.indentationLevelZero let gestureRecognizer = UITapGestureRecognizer( target: self, diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift b/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift index 8de8c014e1..569e01be0d 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsCell.swift @@ -168,7 +168,7 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling { func applySubCellStyling() { contentView.layoutMargins.left = subCellLeadingIndentation - backgroundView?.backgroundColor = UIColor.Cell.Background.indentationLevelOne + backgroundView?.backgroundColor = UIColor.Cell.Background.indentationLevelZero } func setLeadingView(superviewProvider: (UIView) -> Void) { diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsDNSTextCell.swift b/ios/MullvadVPN/View controllers/Settings/SettingsDNSTextCell.swift index ff8b1991ab..b335d686d7 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsDNSTextCell.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsDNSTextCell.swift @@ -114,7 +114,7 @@ class SettingsDNSTextCell: SettingsCell, UITextFieldDelegate { textField.textMargins.left = UIMetrics.SettingsCell.textFieldNonEditingContentInsetLeft textField.textColor = .white - backgroundView?.backgroundColor = UIColor.Cell.Background.indentationLevelOne + backgroundView?.backgroundColor = UIColor.Cell.Background.indentationLevelZero } } diff --git a/ios/MullvadVPN/View controllers/Settings/SwiftUI components/SingleChoiceList.swift b/ios/MullvadVPN/View controllers/Settings/SwiftUI components/SingleChoiceList.swift index 07e547b68e..6449dbf6d5 100644 --- a/ios/MullvadVPN/View controllers/Settings/SwiftUI components/SingleChoiceList.swift +++ b/ios/MullvadVPN/View controllers/Settings/SwiftUI components/SingleChoiceList.swift @@ -200,7 +200,7 @@ struct SingleChoiceList<Value>: View where Value: Equatable { .background( isSelected ? Color(UIColor.Cell.Background.selected) - : Color(UIColor.Cell.Background.indentationLevelOne) + : Color(UIColor.Cell.Background.indentationLevelZero) ) .foregroundColor(Color(UIColor.Cell.titleTextColor)) } diff --git a/ios/MullvadVPN/Views/List/MullvadList.swift b/ios/MullvadVPN/Views/List/MullvadList.swift new file mode 100644 index 0000000000..51b9fe808b --- /dev/null +++ b/ios/MullvadVPN/Views/List/MullvadList.swift @@ -0,0 +1,68 @@ +import SwiftUI + +/// A plain simple list. +/// * separators reaching all the way +/// * the height of all items in the list are based on the highest element +/// * the list is not higher than all its items +/// * transparent background +struct MullvadList<Content: View, Data: RandomAccessCollection<ID>, ID: Hashable>: View { + let data: Data + let content: (Data.Element) -> Content + let id: KeyPath<Data.Element, ID>? + + @State var itemHeight: CGFloat = 0 + var maxListHeight: CGFloat { + var height = itemHeight * CGFloat(data.count) + return height > 0 ? height : .infinity + } + + init(_ data: Data, id: KeyPath<Data.Element, ID>, @ViewBuilder content: @escaping (Data.Element) -> Content) { + self.data = data + self.id = id + self.content = content + } + + init(_ data: Data, @ViewBuilder content: @escaping (Data.Element) -> Content) { + self.data = data + self.content = content + self.id = nil + } + + var body: some View { + List(data, id: id ?? \.self) { item in + content(item) + .sizeOfView { size in + if itemHeight < size.height { + itemHeight = size.height + } + } + .listRowInsets(.init()) + .listSectionSeparator(.hidden, edges: .bottom) + .listRowSeparatorTint(.MullvadList.separator) + .listRowBackground(Color.clear) + .apply { + if #available(iOS 16.0, *) { + $0.alignmentGuide(.listRowSeparatorLeading) { _ in + 0 + } + } else { + $0 + } + } + .frame(height: itemHeight) + } + .listStyle(.plain) + .frame(maxHeight: maxListHeight) + } +} + +#Preview { + MullvadList([1, 2, 3]) { item in + VStack { + ForEach(0 ..< item, id: \.self) { + Text("\($0)") + } + } + .padding() + } +} diff --git a/ios/MullvadVPN/Views/List/MullvadListNavigationItemView.swift b/ios/MullvadVPN/Views/List/MullvadListNavigationItemView.swift new file mode 100644 index 0000000000..0d5223f145 --- /dev/null +++ b/ios/MullvadVPN/Views/List/MullvadListNavigationItemView.swift @@ -0,0 +1,122 @@ +import SwiftUI + +struct MullvadListNavigationItem: Hashable, Identifiable { + let id: UUID + let title: String + let state: String? + let detail: String? + let accessibilityIdentifier: AccessibilityIdentifier? + let didSelect: (() -> Void)? + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(title) + hasher.combine(state) + hasher.combine(detail) + } + + static func == (lhs: MullvadListNavigationItem, rhs: MullvadListNavigationItem) -> Bool { + lhs.id == rhs.id + } +} + +struct MullvadListNavigationItemView: View { + private let title: String + private let state: String? + private let detail: String? + private let accessibilityIdentifier: AccessibilityIdentifier? + private let didSelect: (() -> Void)? + @State private var isPressed = false + init( + item: MullvadListNavigationItem + ) { + self.title = item.title + self.state = item.state.flatMap { $0.isEmpty ? nil : $0 } + self.detail = item.detail.flatMap { $0.isEmpty ? nil : $0 } + self.accessibilityIdentifier = item.accessibilityIdentifier + self.didSelect = item.didSelect + } + + var body: some View { + Button { + didSelect?() + } label: { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(title) + .foregroundStyle(Color(.Cell.titleTextColor)) + .font(.mullvadSmallSemiBold) + if let detail { + Text(detail) + .foregroundStyle(Color(.Cell.detailTextColor.withAlphaComponent(0.6))) + .font(.mullvadMiniSemiBold) + } + } + Spacer() + if let state { + Text(state) + .foregroundStyle(Color(.Cell.titleTextColor.withAlphaComponent(0.6))) + .font(.mullvadTiny) + } + Image(.iconChevron) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .frame(minHeight: UIMetrics.TableView.rowHeight, maxHeight: .infinity) + .background( + isPressed ? Color.MullvadButton.primaryPressed : Color.MullvadButton + .primary + ) + } + .accessibilityIdentifier(accessibilityIdentifier?.asString ?? "") + .onButtonPressedChange { isPressed in + self.isPressed = isPressed + } + } +} + +fileprivate extension View { + func onButtonPressedChange(_ onChange: @escaping (Bool) -> Void) -> some View { + buttonStyle( + MullvadListButtonStyle(onButtonPressedChange: onChange) + ) + } +} + +private struct MullvadListButtonStyle: ButtonStyle { + let onButtonPressedChange: (Bool) -> Void + func makeBody(configuration: Configuration) -> some View { + configuration.label + .onChange(of: configuration.isPressed) { newValue in + onButtonPressedChange(newValue) + } + } +} + +#Preview { + Text("") + .sheet(isPresented: .constant(true)) { + MullvadList( + [ + MullvadListNavigationItem( + id: UUID(), + title: "Test method", + state: "In use", + detail: "Very good method", + accessibilityIdentifier: nil, + didSelect: { print("selected") } + ), + MullvadListNavigationItem( + id: UUID(), + title: "Test method2", + state: "In use", + detail: nil, + accessibilityIdentifier: nil, + didSelect: { print("selected") } + ), + ] + ) { item in + MullvadListNavigationItemView(item: item) + } + } +} diff --git a/ios/MullvadVPN/Views/MainButtonStyle.swift b/ios/MullvadVPN/Views/MainButtonStyle.swift index e2f510431d..0ffd239861 100644 --- a/ios/MullvadVPN/Views/MainButtonStyle.swift +++ b/ios/MullvadVPN/Views/MainButtonStyle.swift @@ -21,8 +21,8 @@ struct MainButtonStyle: ButtonStyle { .frame(minHeight: 44) .foregroundColor( isEnabled - ? UIColor.primaryTextColor.color - : UIColor.primaryTextColor.withAlphaComponent(0.2).color + ? .mullvadTextPrimary + : .mullvadTextPrimaryDisabled ) .background( isEnabled @@ -44,20 +44,34 @@ extension MainButtonStyle { var color: Color { switch self { case .default: - UIColor.primaryColor.color + Color.MullvadButton.primary case .danger: - UIColor.dangerColor.color + Color.MullvadButton.danger case .success: - UIColor.successColor.color + Color.MullvadButton.positive } } var pressedColor: Color { - color.darkened(by: 0.4)! + switch self { + case .default: + Color.MullvadButton.primaryPressed + case .danger: + Color.MullvadButton.dangerPressed + case .success: + Color.MullvadButton.positivePressed + } } var disabledColor: Color { - color.darkened(by: 0.6)! + switch self { + case .default: + Color.MullvadButton.primaryDisabled + case .danger: + Color.MullvadButton.dangerDisabled + case .success: + Color.MullvadButton.positiveDisabled + } } } } diff --git a/ios/MullvadVPN/Views/MullvadInfoHeaderView.swift b/ios/MullvadVPN/Views/MullvadInfoHeaderView.swift new file mode 100644 index 0000000000..3fcbb1f409 --- /dev/null +++ b/ios/MullvadVPN/Views/MullvadInfoHeaderView.swift @@ -0,0 +1,24 @@ +import SwiftUI + +struct MullvadInfoHeaderView: View { + let bodyText: String + let link: String + + let onTapLink: (() -> Void)? + + var body: some View { + var headerText: AttributedString { + var bodyText = AttributedString(bodyText) + bodyText.foregroundColor = Color(.ContentHeading.textColor) + var link = AttributedString(link) + link.foregroundColor = Color(.ContentHeading.linkColor) + return bodyText + link + } + Button { + onTapLink?() + } label: { + Text(headerText) + .multilineTextAlignment(.leading) + } + } +} diff --git a/ios/MullvadVPNUITests/Pages/APIAccessPage.swift b/ios/MullvadVPNUITests/Pages/APIAccessPage.swift index 1ea3a19784..b70e5c9d24 100644 --- a/ios/MullvadVPNUITests/Pages/APIAccessPage.swift +++ b/ios/MullvadVPNUITests/Pages/APIAccessPage.swift @@ -23,10 +23,10 @@ class APIAccessPage: Page { } func getAccessMethodCells() -> [XCUIElement] { - app.otherElements[AccessibilityIdentifier.apiAccessView].cells.allElementsBoundByIndex + app.collectionViews[AccessibilityIdentifier.apiAccessListView].buttons.allElementsBoundByIndex } func getAccessMethodCell(accessibilityId: AccessibilityIdentifier) -> XCUIElement { - app.otherElements[AccessibilityIdentifier.apiAccessView].cells[accessibilityId] + app.buttons[accessibilityId] } } |
