summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2025-05-21 09:48:07 +0200
committerBug Magnet <marco.nikic@mullvad.net>2025-05-21 09:48:07 +0200
commitc5bd16efa266871b3a67b1153d34b42ceb560bae (patch)
treebb20ffd28b48c4f072d7ee3a8e77aa1acb818ce2
parent0e79313bc8ed32d99aae9e41838252867a86c4d8 (diff)
parentc4fe8362013d603c6e2626bba62d5212c3b4b299 (diff)
downloadmullvadvpn-c5bd16efa266871b3a67b1153d34b42ceb560bae.tar.xz
mullvadvpn-c5bd16efa266871b3a67b1153d34b42ceb560bae.zip
Merge branch 'replace-list-view-for-api-access-methods-with-a-swiftui-list-ios-1082'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj48
-rw-r--r--ios/MullvadVPN/Classes/AccessbilityIdentifier.swift1
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift2
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodCoordinator.swift29
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractor.swift9
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodView.swift125
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift259
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewControllerDelegate.swift6
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessViewModelBridge.swift30
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift2
-rw-r--r--ios/MullvadVPN/UI appearance/Color+Mullvad.swift31
-rw-r--r--ios/MullvadVPN/UI appearance/Font+Mullvad.swift25
-rw-r--r--ios/MullvadVPN/UI appearance/UIColor+Palette.swift2
-rw-r--r--ios/MullvadVPN/UI appearance/UIMetrics.swift2
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/LocationCell.swift2
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift4
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsAddDNSEntryCell.swift2
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsCell.swift2
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsDNSTextCell.swift2
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SwiftUI components/SingleChoiceList.swift2
-rw-r--r--ios/MullvadVPN/Views/List/MullvadList.swift68
-rw-r--r--ios/MullvadVPN/Views/List/MullvadListNavigationItemView.swift122
-rw-r--r--ios/MullvadVPN/Views/MainButtonStyle.swift28
-rw-r--r--ios/MullvadVPN/Views/MullvadInfoHeaderView.swift24
-rw-r--r--ios/MullvadVPNUITests/Pages/APIAccessPage.swift4
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]
}
}