summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2024-02-21 14:57:55 +0100
committerBug Magnet <marco.nikic@mullvad.net>2024-02-21 14:57:55 +0100
commit837673a188f62e59d0f4ab40617b07c242dd68a3 (patch)
tree275d1c9515be59bacf0f35e342b001db002a01cb
parentafd04b6a4e6e28fb4583b724983121678a0104e9 (diff)
parented1b81eb4b2aff328f3246cbb9f88731c73f0f0c (diff)
downloadmullvadvpn-837673a188f62e59d0f4ab40617b07c242dd68a3.tar.xz
mullvadvpn-837673a188f62e59d0f4ab40617b07c242dd68a3.zip
Merge branch 'add-ui-for-editing-a-custom-list-ios-513'
-rw-r--r--ios/MullvadSettings/CustomList.swift6
-rw-r--r--ios/MullvadSettings/CustomListRepository.swift4
-rw-r--r--ios/MullvadSettings/CustomListRepositoryProtocol.swift3
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj82
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/AddCustomListCoordinator.swift74
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/CustomListCellConfiguration.swift105
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/CustomListDataSourceConfiguration.swift106
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/CustomListInteractor.swift31
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/CustomListItemIdentifier.swift61
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/CustomListSectionIdentifier.swift16
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/CustomListValidationError.swift31
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/CustomListViewController.swift190
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/CustomListViewModel.swift21
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/EditCustomListCoordinator.swift72
-rw-r--r--ios/MullvadVPN/Coordinators/SelectLocationCoordinator.swift2
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsCellConfiguration.swift4
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsValidationErrorContentConfiguration.swift26
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodValidationError.swift6
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/SettingsFieldValidationErrorConfiguration.swift26
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/SettingsFieldValidationErrorContentView.swift (renamed from ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsValidationErrorContentView.swift)19
-rw-r--r--ios/MullvadVPN/UI appearance/UIMetrics.swift7
-rw-r--r--ios/MullvadVPNTests/CustomListRepositoryTests.swift38
22 files changed, 851 insertions, 79 deletions
diff --git a/ios/MullvadSettings/CustomList.swift b/ios/MullvadSettings/CustomList.swift
index 51066c7281..cc54672e1a 100644
--- a/ios/MullvadSettings/CustomList.swift
+++ b/ios/MullvadSettings/CustomList.swift
@@ -12,9 +12,11 @@ import MullvadTypes
public struct CustomList: Codable, Equatable {
public let id: UUID
public var name: String
- public var list: [RelayLocation] = []
- public init(id: UUID, name: String) {
+ public var locations: [RelayLocation]
+
+ public init(id: UUID = UUID(), name: String, locations: [RelayLocation]) {
self.id = id
self.name = name
+ self.locations = locations
}
}
diff --git a/ios/MullvadSettings/CustomListRepository.swift b/ios/MullvadSettings/CustomListRepository.swift
index e900ff355c..deb0162d16 100644
--- a/ios/MullvadSettings/CustomListRepository.swift
+++ b/ios/MullvadSettings/CustomListRepository.swift
@@ -41,12 +41,12 @@ public struct CustomListRepository: CustomListRepositoryProtocol {
public init() {}
- public func create(_ name: String) throws -> CustomList {
+ public func create(_ name: String, locations: [RelayLocation]) throws -> CustomList {
var lists = fetchAll()
if lists.contains(where: { $0.name == name }) {
throw CustomRelayListError.duplicateName
} else {
- let item = CustomList(id: UUID(), name: name)
+ let item = CustomList(id: UUID(), name: name, locations: locations)
lists.append(item)
try write(lists)
return item
diff --git a/ios/MullvadSettings/CustomListRepositoryProtocol.swift b/ios/MullvadSettings/CustomListRepositoryProtocol.swift
index 42c498d452..582111b15d 100644
--- a/ios/MullvadSettings/CustomListRepositoryProtocol.swift
+++ b/ios/MullvadSettings/CustomListRepositoryProtocol.swift
@@ -28,8 +28,9 @@ public protocol CustomListRepositoryProtocol {
/// Create a custom list by unique name.
/// - Parameter name: a custom list name.
+ /// - Parameter locations: locations in a custom list.
/// - Returns: a persistent custom list model upon success, otherwise throws `Error`.
- func create(_ name: String) throws -> CustomList
+ func create(_ name: String, locations: [RelayLocation]) throws -> CustomList
/// Fetch all custom list.
/// - Returns: all custom list model .
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 6495711e90..c648222b8f 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -501,8 +501,6 @@
7A58699B2B482FE200640D27 /* UITableViewCell+Disable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A58699A2B482FE200640D27 /* UITableViewCell+Disable.swift */; };
7A58699F2B50057100640D27 /* AccessMethodKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A58699D2B50057100640D27 /* AccessMethodKind.swift */; };
7A5869A22B502EA800640D27 /* MethodSettingsSectionIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869A12B502EA700640D27 /* MethodSettingsSectionIdentifier.swift */; };
- 7A5869A62B51405900640D27 /* MethodSettingsValidationErrorContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869A52B51405900640D27 /* MethodSettingsValidationErrorContentConfiguration.swift */; };
- 7A5869A82B5140C200640D27 /* MethodSettingsValidationErrorContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869A72B5140C200640D27 /* MethodSettingsValidationErrorContentView.swift */; };
7A5869AB2B55527C00640D27 /* IPOverrideCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869AA2B55527C00640D27 /* IPOverrideCoordinator.swift */; };
7A5869AD2B5552E200640D27 /* IPOverrideViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869AC2B5552E200640D27 /* IPOverrideViewController.swift */; };
7A5869B72B56B41500640D27 /* IPOverrideTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869B62B56B41500640D27 /* IPOverrideTextViewController.swift */; };
@@ -518,6 +516,18 @@
7A6000F92B6273A4001CF0D9 /* AccessMethodViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D7B2B03BDD100E7CDD7 /* AccessMethodViewModel.swift */; };
7A6000FC2B628DF6001CF0D9 /* ListCellContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6000FB2B628DF6001CF0D9 /* ListCellContentConfiguration.swift */; };
7A6000FE2B628E9F001CF0D9 /* ListCellContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6000FD2B628E9F001CF0D9 /* ListCellContentView.swift */; };
+ 7A6389DB2B7E3BD6008E77E1 /* CustomListCellConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389D22B7E3BD6008E77E1 /* CustomListCellConfiguration.swift */; };
+ 7A6389DC2B7E3BD6008E77E1 /* CustomListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389D32B7E3BD6008E77E1 /* CustomListViewModel.swift */; };
+ 7A6389DD2B7E3BD6008E77E1 /* CustomListDataSourceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389D42B7E3BD6008E77E1 /* CustomListDataSourceConfiguration.swift */; };
+ 7A6389DE2B7E3BD6008E77E1 /* CustomListItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389D52B7E3BD6008E77E1 /* CustomListItemIdentifier.swift */; };
+ 7A6389DF2B7E3BD6008E77E1 /* AddCustomListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389D72B7E3BD6008E77E1 /* AddCustomListCoordinator.swift */; };
+ 7A6389E12B7E3BD6008E77E1 /* CustomListSectionIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389D92B7E3BD6008E77E1 /* CustomListSectionIdentifier.swift */; };
+ 7A6389E22B7E3BD6008E77E1 /* CustomListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389DA2B7E3BD6008E77E1 /* CustomListInteractor.swift */; };
+ 7A6389E52B7E4247008E77E1 /* EditCustomListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389E42B7E4247008E77E1 /* EditCustomListCoordinator.swift */; };
+ 7A6389E72B7E42BE008E77E1 /* CustomListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389E62B7E42BE008E77E1 /* CustomListViewController.swift */; };
+ 7A6389E92B7F8FE2008E77E1 /* CustomListValidationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389E82B7F8FE2008E77E1 /* CustomListValidationError.swift */; };
+ 7A6389EB2B7FAD7A008E77E1 /* SettingsFieldValidationErrorContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389EA2B7FAD7A008E77E1 /* SettingsFieldValidationErrorContentView.swift */; };
+ 7A6389ED2B7FADA1008E77E1 /* SettingsFieldValidationErrorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389EC2B7FADA1008E77E1 /* SettingsFieldValidationErrorConfiguration.swift */; };
7A6B4F592AB8412E00123853 /* TunnelMonitorTimings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */; };
7A6F2FA52AFA3CB2006D0856 /* AccountExpiryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */; };
7A6F2FA72AFBB9AE006D0856 /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */; };
@@ -785,14 +795,14 @@
F03580252A13842C00E5DAFD /* IncreasedHitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */; };
F04F95A12B21D24400431E08 /* shadowsocks.h in Headers */ = {isa = PBXBuildFile; fileRef = F04F95A02B21D24400431E08 /* shadowsocks.h */; settings = {ATTRIBUTES = (Private, ); }; };
F04FBE612A8379EE009278D7 /* AppPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = F04FBE602A8379EE009278D7 /* AppPreferences.swift */; };
- F050AE572B7376C6003F4EDB /* CustomListRepositoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE552B7376C5003F4EDB /* CustomListRepositoryProtocol.swift */; };
- F050AE582B7376C6003F4EDB /* CustomListRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE562B7376C6003F4EDB /* CustomListRepository.swift */; };
- F050AE5A2B7376F4003F4EDB /* CustomList.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE592B7376F4003F4EDB /* CustomList.swift */; };
- F050AE5C2B73797D003F4EDB /* CustomListRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE5B2B73797D003F4EDB /* CustomListRepositoryTests.swift */; };
F050AE4C2B70D5A7003F4EDB /* SelectLocationNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE4B2B70D5A7003F4EDB /* SelectLocationNode.swift */; };
F050AE4E2B70D7F8003F4EDB /* LocationCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE4D2B70D7F8003F4EDB /* LocationCellViewModel.swift */; };
F050AE502B70DC4F003F4EDB /* SelectLocationNodeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE4F2B70DC4F003F4EDB /* SelectLocationNodeProtocol.swift */; };
F050AE522B70DFC0003F4EDB /* SelectLocationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE512B70DFC0003F4EDB /* SelectLocationSection.swift */; };
+ F050AE572B7376C6003F4EDB /* CustomListRepositoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE552B7376C5003F4EDB /* CustomListRepositoryProtocol.swift */; };
+ F050AE582B7376C6003F4EDB /* CustomListRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE562B7376C6003F4EDB /* CustomListRepository.swift */; };
+ F050AE5A2B7376F4003F4EDB /* CustomList.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE592B7376F4003F4EDB /* CustomList.swift */; };
+ F050AE5C2B73797D003F4EDB /* CustomListRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE5B2B73797D003F4EDB /* CustomListRepositoryTests.swift */; };
F050AE5E2B739A73003F4EDB /* LocationDataSourceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE5D2B739A73003F4EDB /* LocationDataSourceProtocol.swift */; };
F050AE602B73A41E003F4EDB /* AllLocationDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE5F2B73A41E003F4EDB /* AllLocationDataSource.swift */; };
F050AE622B74DBAC003F4EDB /* CustomListsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE612B74DBAC003F4EDB /* CustomListsDataSource.swift */; };
@@ -1718,8 +1728,6 @@
7A58699A2B482FE200640D27 /* UITableViewCell+Disable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+Disable.swift"; sourceTree = "<group>"; };
7A58699D2B50057100640D27 /* AccessMethodKind.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessMethodKind.swift; sourceTree = "<group>"; };
7A5869A12B502EA700640D27 /* MethodSettingsSectionIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MethodSettingsSectionIdentifier.swift; sourceTree = "<group>"; };
- 7A5869A52B51405900640D27 /* MethodSettingsValidationErrorContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSettingsValidationErrorContentConfiguration.swift; sourceTree = "<group>"; };
- 7A5869A72B5140C200640D27 /* MethodSettingsValidationErrorContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSettingsValidationErrorContentView.swift; sourceTree = "<group>"; };
7A5869AA2B55527C00640D27 /* IPOverrideCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideCoordinator.swift; sourceTree = "<group>"; };
7A5869AC2B5552E200640D27 /* IPOverrideViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideViewController.swift; sourceTree = "<group>"; };
7A5869B22B5697AC00640D27 /* IPOverride.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverride.swift; sourceTree = "<group>"; };
@@ -1734,6 +1742,18 @@
7A6000F52B60092F001CF0D9 /* AccessMethodViewModelEditing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodViewModelEditing.swift; sourceTree = "<group>"; };
7A6000FB2B628DF6001CF0D9 /* ListCellContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListCellContentConfiguration.swift; sourceTree = "<group>"; };
7A6000FD2B628E9F001CF0D9 /* ListCellContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListCellContentView.swift; sourceTree = "<group>"; };
+ 7A6389D22B7E3BD6008E77E1 /* CustomListCellConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListCellConfiguration.swift; sourceTree = "<group>"; };
+ 7A6389D32B7E3BD6008E77E1 /* CustomListViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListViewModel.swift; sourceTree = "<group>"; };
+ 7A6389D42B7E3BD6008E77E1 /* CustomListDataSourceConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListDataSourceConfiguration.swift; sourceTree = "<group>"; };
+ 7A6389D52B7E3BD6008E77E1 /* CustomListItemIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListItemIdentifier.swift; sourceTree = "<group>"; };
+ 7A6389D72B7E3BD6008E77E1 /* AddCustomListCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddCustomListCoordinator.swift; sourceTree = "<group>"; };
+ 7A6389D92B7E3BD6008E77E1 /* CustomListSectionIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListSectionIdentifier.swift; sourceTree = "<group>"; };
+ 7A6389DA2B7E3BD6008E77E1 /* CustomListInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListInteractor.swift; sourceTree = "<group>"; };
+ 7A6389E42B7E4247008E77E1 /* EditCustomListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCustomListCoordinator.swift; sourceTree = "<group>"; };
+ 7A6389E62B7E42BE008E77E1 /* CustomListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListViewController.swift; sourceTree = "<group>"; };
+ 7A6389E82B7F8FE2008E77E1 /* CustomListValidationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListValidationError.swift; sourceTree = "<group>"; };
+ 7A6389EA2B7FAD7A008E77E1 /* SettingsFieldValidationErrorContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFieldValidationErrorContentView.swift; sourceTree = "<group>"; };
+ 7A6389EC2B7FADA1008E77E1 /* SettingsFieldValidationErrorConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFieldValidationErrorConfiguration.swift; sourceTree = "<group>"; };
7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorTimings.swift; sourceTree = "<group>"; };
7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryTests.swift; sourceTree = "<group>"; };
7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiry.swift; sourceTree = "<group>"; };
@@ -1901,14 +1921,14 @@
F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncreasedHitButton.swift; sourceTree = "<group>"; };
F04F95A02B21D24400431E08 /* shadowsocks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = shadowsocks.h; sourceTree = "<group>"; };
F04FBE602A8379EE009278D7 /* AppPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPreferences.swift; sourceTree = "<group>"; };
- F050AE552B7376C5003F4EDB /* CustomListRepositoryProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListRepositoryProtocol.swift; sourceTree = "<group>"; };
- F050AE562B7376C6003F4EDB /* CustomListRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListRepository.swift; sourceTree = "<group>"; };
- F050AE592B7376F4003F4EDB /* CustomList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomList.swift; sourceTree = "<group>"; };
- F050AE5B2B73797D003F4EDB /* CustomListRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListRepositoryTests.swift; sourceTree = "<group>"; };
F050AE4B2B70D5A7003F4EDB /* SelectLocationNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationNode.swift; sourceTree = "<group>"; };
F050AE4D2B70D7F8003F4EDB /* LocationCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationCellViewModel.swift; sourceTree = "<group>"; };
F050AE4F2B70DC4F003F4EDB /* SelectLocationNodeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationNodeProtocol.swift; sourceTree = "<group>"; };
F050AE512B70DFC0003F4EDB /* SelectLocationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationSection.swift; sourceTree = "<group>"; };
+ F050AE552B7376C5003F4EDB /* CustomListRepositoryProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListRepositoryProtocol.swift; sourceTree = "<group>"; };
+ F050AE562B7376C6003F4EDB /* CustomListRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListRepository.swift; sourceTree = "<group>"; };
+ F050AE592B7376F4003F4EDB /* CustomList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomList.swift; sourceTree = "<group>"; };
+ F050AE5B2B73797D003F4EDB /* CustomListRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListRepositoryTests.swift; sourceTree = "<group>"; };
F050AE5D2B739A73003F4EDB /* LocationDataSourceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationDataSourceProtocol.swift; sourceTree = "<group>"; };
F050AE5F2B73A41E003F4EDB /* AllLocationDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllLocationDataSource.swift; sourceTree = "<group>"; };
F050AE612B74DBAC003F4EDB /* CustomListsDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListsDataSource.swift; sourceTree = "<group>"; };
@@ -2281,8 +2301,6 @@
7A5869C62B5A8E4C00640D27 /* MethodSettingsDataSourceConfiguration.swift */,
5827B0952B0DB2C100CCBBA1 /* MethodSettingsItemIdentifier.swift */,
7A5869A12B502EA700640D27 /* MethodSettingsSectionIdentifier.swift */,
- 7A5869A52B51405900640D27 /* MethodSettingsValidationErrorContentConfiguration.swift */,
- 7A5869A72B5140C200640D27 /* MethodSettingsValidationErrorContentView.swift */,
5827B0912B0CAB2800CCBBA1 /* MethodSettingsViewController.swift */,
5827B0AD2B0F4CBE00CCBBA1 /* MethodSettingsViewControllerDelegate.swift */,
5827B0B82B14A1C700CCBBA1 /* MethodTestingStatusCellContentConfiguration.swift */,
@@ -3024,6 +3042,8 @@
58CAF9F22983D32200BE19F7 /* Coordinators */ = {
isa = PBXGroup;
children = (
+ 7A6389D12B7E3BD6008E77E1 /* CustomLists */,
+ 58EFC76F2AFB3FA800E9F4CB /* Settings */,
7A9CCCAF2A96302800DD6A34 /* AccountCoordinator.swift */,
7A9CCCAC2A96302800DD6A34 /* AccountDeletionCoordinator.swift */,
7A9CCCA32A96302700DD6A34 /* AddCreditSucceededCoordinator.swift */,
@@ -3039,7 +3059,6 @@
7A9CCCA52A96302700DD6A34 /* RevokedCoordinator.swift */,
7A9CCCB02A96302800DD6A34 /* SafariCoordinator.swift */,
7A9CCCA72A96302700DD6A34 /* SelectLocationCoordinator.swift */,
- 58EFC76F2AFB3FA800E9F4CB /* Settings */,
7A9CCCA62A96302700DD6A34 /* SetupAccountCompletedCoordinator.swift */,
7A9CCCA22A96302700DD6A34 /* TermsOfServiceCoordinator.swift */,
7A9CCCB22A96302800DD6A34 /* TunnelCoordinator.swift */,
@@ -3320,6 +3339,8 @@
7A5869A92B55516700640D27 /* IPOverride */,
58EFC7702AFB45E500E9F4CB /* SettingsChildCoordinator.swift */,
7A9CCCAD2A96302800DD6A34 /* SettingsCoordinator.swift */,
+ 7A6389EC2B7FADA1008E77E1 /* SettingsFieldValidationErrorConfiguration.swift */,
+ 7A6389EA2B7FAD7A008E77E1 /* SettingsFieldValidationErrorContentView.swift */,
);
path = Settings;
sourceTree = "<group>";
@@ -3402,6 +3423,23 @@
path = IPOverride;
sourceTree = "<group>";
};
+ 7A6389D12B7E3BD6008E77E1 /* CustomLists */ = {
+ isa = PBXGroup;
+ children = (
+ 7A6389D72B7E3BD6008E77E1 /* AddCustomListCoordinator.swift */,
+ 7A6389D22B7E3BD6008E77E1 /* CustomListCellConfiguration.swift */,
+ 7A6389D42B7E3BD6008E77E1 /* CustomListDataSourceConfiguration.swift */,
+ 7A6389DA2B7E3BD6008E77E1 /* CustomListInteractor.swift */,
+ 7A6389D52B7E3BD6008E77E1 /* CustomListItemIdentifier.swift */,
+ 7A6389D92B7E3BD6008E77E1 /* CustomListSectionIdentifier.swift */,
+ 7A6389E82B7F8FE2008E77E1 /* CustomListValidationError.swift */,
+ 7A6389E62B7E42BE008E77E1 /* CustomListViewController.swift */,
+ 7A6389D32B7E3BD6008E77E1 /* CustomListViewModel.swift */,
+ 7A6389E42B7E4247008E77E1 /* EditCustomListCoordinator.swift */,
+ );
+ path = CustomLists;
+ sourceTree = "<group>";
+ };
7A83C3FC2A55B39500DFB83A /* TestPlans */ = {
isa = PBXGroup;
children = (
@@ -4929,6 +4967,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 7A6389DC2B7E3BD6008E77E1 /* CustomListViewModel.swift in Sources */,
7A9CCCC42A96302800DD6A34 /* TunnelCoordinator.swift in Sources */,
5827B0A42B0F38FD00CCBBA1 /* EditAccessMethodInteractorProtocol.swift in Sources */,
586C0D852B03D31E00E7CDD7 /* SocksSectionHandler.swift in Sources */,
@@ -4977,6 +5016,7 @@
7A516C2E2B6D357500BBD33D /* URL+Scoping.swift in Sources */,
5878A27529093A310096FC88 /* StorePaymentEvent.swift in Sources */,
7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */,
+ 7A6389EB2B7FAD7A008E77E1 /* SettingsFieldValidationErrorContentView.swift in Sources */,
58B26E2A2943545A00D5980C /* NotificationManagerDelegate.swift in Sources */,
58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */,
5878A27B2909649A0096FC88 /* CustomOverlayRenderer.swift in Sources */,
@@ -5041,6 +5081,7 @@
58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */,
F09A29822A9F8AD200EA3B6F /* RedeemVoucherInteractor.swift in Sources */,
58138E61294871C600684F0C /* DeviceDataThrottling.swift in Sources */,
+ 7A6389ED2B7FADA1008E77E1 /* SettingsFieldValidationErrorConfiguration.swift in Sources */,
5878A279290954790096FC88 /* TunnelViewControllerInteractor.swift in Sources */,
7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */,
7A9CCCBF2A96302800DD6A34 /* SettingsCoordinator.swift in Sources */,
@@ -5074,6 +5115,7 @@
58FF9FE02B075ABC00E4C97D /* EditAccessMethodViewController.swift in Sources */,
F050AE622B74DBAC003F4EDB /* CustomListsDataSource.swift in Sources */,
F0DA87472A9CB9A2006044F1 /* AccountExpiryRow.swift in Sources */,
+ 7A6389E92B7F8FE2008E77E1 /* CustomListValidationError.swift in Sources */,
585CA70F25F8C44600B47C62 /* UIMetrics.swift in Sources */,
E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */,
58CC40EF24A601900019D96E /* ObserverList.swift in Sources */,
@@ -5095,6 +5137,7 @@
58E25F812837BBBB002CFB2C /* SceneDelegate.swift in Sources */,
7A1A26492A29D48A00B978AA /* RelayFilterCellFactory.swift in Sources */,
5867771629097C5B006F721F /* ProductState.swift in Sources */,
+ 7A6389DE2B7E3BD6008E77E1 /* CustomListItemIdentifier.swift in Sources */,
58C76A082A33850E00100D75 /* ApplicationTarget.swift in Sources */,
58CEB3042AFD36CE00E6E088 /* SwitchCellContentView.swift in Sources */,
F07BF2622A26279100042943 /* RedeemVoucherOperation.swift in Sources */,
@@ -5111,8 +5154,11 @@
7AF9BE8E2A331C7B00DBFEDB /* RelayFilterViewModel.swift in Sources */,
58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */,
5864AF0829C78849005B0CD9 /* CellFactoryProtocol.swift in Sources */,
+ 7A6389E22B7E3BD6008E77E1 /* CustomListInteractor.swift in Sources */,
F07CFF2029F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift in Sources */,
+ 7A6389E12B7E3BD6008E77E1 /* CustomListSectionIdentifier.swift in Sources */,
58CEB2F32AFD0BA100E6E088 /* TextCellContentView.swift in Sources */,
+ 7A6389E72B7E42BE008E77E1 /* CustomListViewController.swift in Sources */,
586C0D7C2B03BDD100E7CDD7 /* AccessMethodViewModel.swift in Sources */,
587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */,
7A6F2FA72AFBB9AE006D0856 /* AccountExpiry.swift in Sources */,
@@ -5132,6 +5178,7 @@
5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */,
58B26E242943520C00D5980C /* NotificationProviderProtocol.swift in Sources */,
5877F94E2A0A59AA0052D9E9 /* NotificationResponse.swift in Sources */,
+ 7A6389E52B7E4247008E77E1 /* EditCustomListCoordinator.swift in Sources */,
58677712290976FB006F721F /* SettingsInteractor.swift in Sources */,
58EF875D2B1638BF00C098B2 /* ProxyConfigurationTesterProtocol.swift in Sources */,
58CE5E66224146200008646E /* LoginViewController.swift in Sources */,
@@ -5192,6 +5239,7 @@
581DA2752A1E283E0046ED47 /* WgKeyRotation.swift in Sources */,
5827B0BB2B14A28300CCBBA1 /* MethodTestingStatusCellContentView.swift in Sources */,
7A83C4022A57FAA800DFB83A /* SettingsDNSInfoCell.swift in Sources */,
+ 7A6389DF2B7E3BD6008E77E1 /* AddCustomListCoordinator.swift in Sources */,
586C0D952B03D92100E7CDD7 /* SocksItemIdentifier.swift in Sources */,
F0C6A8432AB08E54000777A8 /* RedeemVoucherViewConfiguration.swift in Sources */,
7AF10EB42ADE85BC00C090B9 /* RelayFilterCoordinator.swift in Sources */,
@@ -5230,11 +5278,11 @@
584D26C4270C855B004EA533 /* PreferencesDataSource.swift in Sources */,
F0D8825B2B04F53600D3EF9A /* OutgoingConnectionData.swift in Sources */,
7A6F2FAF2AFE36E7006D0856 /* PreferencesInfoButtonItem.swift in Sources */,
+ 7A6389DD2B7E3BD6008E77E1 /* CustomListDataSourceConfiguration.swift in Sources */,
5827B0BF2B14B37D00CCBBA1 /* Publisher+PreviousValue.swift in Sources */,
7A9CCCB62A96302800DD6A34 /* OutOfTimeCoordinator.swift in Sources */,
5827B0AA2B0F4C9100CCBBA1 /* EditAccessMethodViewControllerDelegate.swift in Sources */,
F050AE5E2B739A73003F4EDB /* LocationDataSourceProtocol.swift in Sources */,
- 7A5869A82B5140C200640D27 /* MethodSettingsValidationErrorContentView.swift in Sources */,
A99E5EE22B762ED30033F241 /* ProblemReportViewController+ViewManagement.swift in Sources */,
7A5869A22B502EA800640D27 /* MethodSettingsSectionIdentifier.swift in Sources */,
586C0D812B03CA8400E7CDD7 /* CurrentValueSubject+UIActionBindings.swift in Sources */,
@@ -5244,11 +5292,11 @@
58B43C1925F77DB60002C8C3 /* TunnelControlView.swift in Sources */,
F09A297B2A9F8A9B00EA3B6F /* LogoutDialogueView.swift in Sources */,
58CEB2FB2AFD13E600E6E088 /* UIListContentConfiguration+Extensions.swift in Sources */,
- 7A5869A62B51405900640D27 /* MethodSettingsValidationErrorContentConfiguration.swift in Sources */,
5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */,
7A21DACF2A30AA3700A787A9 /* UITextField+Appearance.swift in Sources */,
585B1FF02AB09F97008AD470 /* VPNConnectionProtocol.swift in Sources */,
58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */,
+ 7A6389DB2B7E3BD6008E77E1 /* CustomListCellConfiguration.swift in Sources */,
F09A297C2A9F8A9B00EA3B6F /* VoucherTextField.swift in Sources */,
7A5869B72B56B41500640D27 /* IPOverrideTextViewController.swift in Sources */,
58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */,
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/AddCustomListCoordinator.swift b/ios/MullvadVPN/Coordinators/CustomLists/AddCustomListCoordinator.swift
new file mode 100644
index 0000000000..c85147f37c
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/CustomLists/AddCustomListCoordinator.swift
@@ -0,0 +1,74 @@
+//
+// AddCustomListCoordinator.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-02-14.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Combine
+import MullvadSettings
+import Routing
+import UIKit
+
+class AddCustomListCoordinator: Coordinator, Presentable, Presenting {
+ let navigationController: UINavigationController
+ let customListInteractor: CustomListInteractorProtocol
+
+ var presentedViewController: UIViewController {
+ navigationController
+ }
+
+ var didFinish: (() -> Void)?
+
+ init(
+ navigationController: UINavigationController,
+ customListInteractor: CustomListInteractorProtocol
+ ) {
+ self.navigationController = navigationController
+ self.customListInteractor = customListInteractor
+ }
+
+ func start() {
+ let subject = CurrentValueSubject<CustomListViewModel, Never>(
+ CustomListViewModel(id: UUID(), name: "", locations: [], tableSections: [.name, .addLocations])
+ )
+
+ let controller = CustomListViewController(
+ interactor: customListInteractor,
+ subject: subject,
+ alertPresenter: AlertPresenter(context: self)
+ )
+ controller.delegate = self
+
+ controller.navigationItem.title = NSLocalizedString(
+ "CUSTOM_LIST_NAVIGATION_EDIT_TITLE",
+ tableName: "CustomLists",
+ value: "New custom list",
+ comment: ""
+ )
+
+ controller.saveBarButton.title = NSLocalizedString(
+ "CUSTOM_LIST_NAVIGATION_CREATE_BUTTON",
+ tableName: "CustomLists",
+ value: "Create",
+ comment: ""
+ )
+
+ navigationController.pushViewController(controller, animated: false)
+ }
+}
+
+extension AddCustomListCoordinator: CustomListViewControllerDelegate {
+ func customListDidSave() {
+ didFinish?()
+ }
+
+ func customListDidDelete() {
+ // No op.
+ }
+
+ func showLocations() {
+ // TODO: Show view controller for locations.
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/CustomListCellConfiguration.swift b/ios/MullvadVPN/Coordinators/CustomLists/CustomListCellConfiguration.swift
new file mode 100644
index 0000000000..b1db9122eb
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/CustomLists/CustomListCellConfiguration.swift
@@ -0,0 +1,105 @@
+//
+// CustomListCellConfiguration.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-02-14.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Combine
+import UIKit
+
+struct CustomListCellConfiguration {
+ let tableView: UITableView
+ let subject: CurrentValueSubject<CustomListViewModel, Never>
+
+ var onDelete: (() -> Void)?
+
+ func dequeueCell(
+ at indexPath: IndexPath,
+ for itemIdentifier: CustomListItemIdentifier,
+ validationErrors: Set<CustomListFieldValidationError>
+ ) -> UITableViewCell {
+ let cell = tableView.dequeueReusableView(withIdentifier: itemIdentifier.cellIdentifier, for: indexPath)
+
+ configureBackground(cell: cell, itemIdentifier: itemIdentifier, validationErrors: validationErrors)
+
+ switch itemIdentifier {
+ case .name:
+ configureName(cell, itemIdentifier: itemIdentifier)
+ case .addLocations, .editLocations:
+ configureLocations(cell, itemIdentifier: itemIdentifier)
+ case .deleteList:
+ configureDelete(cell, itemIdentifier: itemIdentifier)
+ }
+
+ return cell
+ }
+
+ private func configureBackground(
+ cell: UITableViewCell,
+ itemIdentifier: CustomListItemIdentifier,
+ validationErrors: Set<CustomListFieldValidationError>
+ ) {
+ configureErrorState(
+ cell: cell,
+ itemIdentifier: itemIdentifier,
+ contentValidationErrors: validationErrors
+ )
+
+ guard let cell = cell as? DynamicBackgroundConfiguration else { return }
+
+ cell.setAutoAdaptingBackgroundConfiguration(.mullvadListGroupedCell(), selectionType: .dimmed)
+ }
+
+ private func configureErrorState(
+ cell: UITableViewCell,
+ itemIdentifier: CustomListItemIdentifier,
+ contentValidationErrors: Set<CustomListFieldValidationError>
+ ) {
+ let itemsWithErrors = CustomListItemIdentifier.fromFieldValidationErrors(contentValidationErrors)
+
+ if itemsWithErrors.contains(itemIdentifier) {
+ cell.layer.cornerRadius = 10
+ cell.layer.borderWidth = 1
+ cell.layer.borderColor = UIColor.Cell.validationErrorBorderColor.cgColor
+ } else {
+ cell.layer.borderWidth = 0
+ }
+ }
+
+ private func configureName(_ cell: UITableViewCell, itemIdentifier: CustomListItemIdentifier) {
+ var contentConfiguration = TextCellContentConfiguration()
+
+ contentConfiguration.text = itemIdentifier.text
+ contentConfiguration.setPlaceholder(type: .required)
+ contentConfiguration.textFieldProperties = .withSmartFeaturesDisabled()
+ contentConfiguration.inputText = subject.value.name
+ contentConfiguration.editingEvents.onChange = subject.bindTextAction(to: \.name)
+
+ cell.contentConfiguration = contentConfiguration
+ }
+
+ private func configureLocations(_ cell: UITableViewCell, itemIdentifier: CustomListItemIdentifier) {
+ var contentConfiguration = UIListContentConfiguration.mullvadValueCell(tableStyle: tableView.style)
+
+ contentConfiguration.text = itemIdentifier.text
+ cell.contentConfiguration = contentConfiguration
+
+ if let cell = cell as? CustomCellDisclosureHandling {
+ cell.disclosureType = .chevron
+ }
+ }
+
+ private func configureDelete(_ cell: UITableViewCell, itemIdentifier: CustomListItemIdentifier) {
+ var contentConfiguration = ButtonCellContentConfiguration()
+
+ contentConfiguration.style = .tableInsetGroupedDanger
+ contentConfiguration.text = itemIdentifier.text
+ contentConfiguration.primaryAction = UIAction { _ in
+ onDelete?()
+ }
+
+ cell.contentConfiguration = contentConfiguration
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/CustomListDataSourceConfiguration.swift b/ios/MullvadVPN/Coordinators/CustomLists/CustomListDataSourceConfiguration.swift
new file mode 100644
index 0000000000..faf4e17764
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/CustomLists/CustomListDataSourceConfiguration.swift
@@ -0,0 +1,106 @@
+//
+// CustomListDataSourceConfigurationv.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-02-14.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+class CustomListDataSourceConfiguration: NSObject {
+ let dataSource: UITableViewDiffableDataSource<CustomListSectionIdentifier, CustomListItemIdentifier>
+ var validationErrors: Set<CustomListFieldValidationError> = []
+
+ var didSelectItem: ((CustomListItemIdentifier) -> Void)?
+
+ init(dataSource: UITableViewDiffableDataSource<CustomListSectionIdentifier, CustomListItemIdentifier>) {
+ self.dataSource = dataSource
+ }
+
+ func updateDataSource(
+ sections: [CustomListSectionIdentifier],
+ validationErrors: Set<CustomListFieldValidationError>,
+ animated: Bool,
+ completion: (() -> Void)? = nil
+ ) {
+ var snapshot = NSDiffableDataSourceSnapshot<CustomListSectionIdentifier, CustomListItemIdentifier>()
+
+ sections.forEach { section in
+ switch section {
+ case .name:
+ snapshot.appendSections([.name])
+ snapshot.appendItems([.name], toSection: .name)
+ case .addLocations:
+ snapshot.appendSections([.addLocations])
+ snapshot.appendItems([.addLocations], toSection: .addLocations)
+ case .editLocations:
+ snapshot.appendSections([.editLocations])
+ snapshot.appendItems([.editLocations], toSection: .editLocations)
+ case .deleteList:
+ snapshot.appendSections([.deleteList])
+ snapshot.appendItems([.deleteList], toSection: .deleteList)
+ }
+ }
+
+ dataSource.apply(snapshot, animatingDifferences: animated)
+ }
+
+ func set(validationErrors: Set<CustomListFieldValidationError>) {
+ self.validationErrors = validationErrors
+
+ var snapshot = dataSource.snapshot()
+
+ validationErrors.forEach { error in
+ switch error {
+ case .name:
+ snapshot.reloadSections([.name])
+ }
+ }
+
+ dataSource.apply(snapshot, animatingDifferences: false)
+ }
+}
+
+extension CustomListDataSourceConfiguration: UITableViewDelegate {
+ func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+ UIMetrics.SettingsCell.customListsCellHeight
+ }
+
+ func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
+ let snapshot = dataSource.snapshot()
+
+ let sectionIdentifier = snapshot.sectionIdentifiers[section]
+ let itemsInSection = snapshot.itemIdentifiers(inSection: sectionIdentifier)
+
+ let itemsWithErrors = CustomListItemIdentifier.fromFieldValidationErrors(validationErrors)
+ let errorsInSection = itemsWithErrors.filter { itemsInSection.contains($0) }.compactMap { item in
+ switch item {
+ case .name:
+ CustomListFieldValidationError.name
+ case .addLocations, .editLocations, .deleteList:
+ nil
+ }
+ }
+
+ switch sectionIdentifier {
+ case .name:
+ let view = SettingsFieldValidationErrorContentView(
+ configuration: SettingsFieldValidationErrorConfiguration(
+ errors: errorsInSection.settingsFieldValidationErrors
+ )
+ )
+ return view
+ default:
+ return nil
+ }
+ }
+
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ tableView.deselectRow(at: indexPath, animated: false)
+
+ if let item = dataSource.itemIdentifier(for: indexPath) {
+ didSelectItem?(item)
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/CustomListInteractor.swift b/ios/MullvadVPN/Coordinators/CustomLists/CustomListInteractor.swift
new file mode 100644
index 0000000000..5f129c79cf
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/CustomLists/CustomListInteractor.swift
@@ -0,0 +1,31 @@
+//
+// CustomListInteractor.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-02-15.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadSettings
+
+protocol CustomListInteractorProtocol {
+ func createCustomList(viewModel: CustomListViewModel) throws
+ func updateCustomList(viewModel: CustomListViewModel)
+ func deleteCustomList(id: UUID)
+}
+
+struct CustomListInteractor: CustomListInteractorProtocol {
+ let repository: CustomListRepositoryProtocol
+
+ func createCustomList(viewModel: CustomListViewModel) throws {
+ try _ = repository.create(viewModel.name, locations: viewModel.locations)
+ }
+
+ func updateCustomList(viewModel: CustomListViewModel) {
+ repository.update(viewModel.customList)
+ }
+
+ func deleteCustomList(id: UUID) {
+ repository.delete(id: id)
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/CustomListItemIdentifier.swift b/ios/MullvadVPN/Coordinators/CustomLists/CustomListItemIdentifier.swift
new file mode 100644
index 0000000000..f2cc7726a4
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/CustomLists/CustomListItemIdentifier.swift
@@ -0,0 +1,61 @@
+//
+// CustomListItemIdentifier.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-02-14.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+enum CustomListItemIdentifier: Hashable, CaseIterable {
+ case name
+ case addLocations
+ case editLocations
+ case deleteList
+
+ enum CellIdentifier: String, CellIdentifierProtocol {
+ case name
+ case locations
+ case delete
+
+ var cellClass: AnyClass {
+ BasicCell.self
+ }
+ }
+
+ var cellIdentifier: CellIdentifier {
+ switch self {
+ case .name:
+ .name
+ case .addLocations:
+ .locations
+ case .editLocations:
+ .locations
+ case .deleteList:
+ .delete
+ }
+ }
+
+ var text: String? {
+ switch self {
+ case .name:
+ NSLocalizedString("NAME", tableName: "CustomLists", value: "Name", comment: "")
+ case .addLocations:
+ NSLocalizedString("ADD", tableName: "CustomLists", value: "Add locations", comment: "")
+ case .editLocations:
+ NSLocalizedString("EDIT", tableName: "CustomLists", value: "Edit locations", comment: "")
+ case .deleteList:
+ NSLocalizedString("Delete", tableName: "CustomLists", value: "Delete list", comment: "")
+ }
+ }
+
+ static func fromFieldValidationErrors(_ errors: Set<CustomListFieldValidationError>) -> [CustomListItemIdentifier] {
+ errors.compactMap { error in
+ switch error {
+ case .name:
+ .name
+ }
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/CustomListSectionIdentifier.swift b/ios/MullvadVPN/Coordinators/CustomLists/CustomListSectionIdentifier.swift
new file mode 100644
index 0000000000..847a44a57f
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/CustomLists/CustomListSectionIdentifier.swift
@@ -0,0 +1,16 @@
+//
+// CustomListSectionIdentifier.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-02-14.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+enum CustomListSectionIdentifier: Hashable, CaseIterable {
+ case name
+ case addLocations
+ case editLocations
+ case deleteList
+}
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/CustomListValidationError.swift b/ios/MullvadVPN/Coordinators/CustomLists/CustomListValidationError.swift
new file mode 100644
index 0000000000..100cff15a6
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/CustomLists/CustomListValidationError.swift
@@ -0,0 +1,31 @@
+//
+// CustomListValidationError.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-02-16.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+enum CustomListFieldValidationError: LocalizedError {
+ case name
+
+ var errorDescription: String? {
+ switch self {
+ case .name:
+ NSLocalizedString(
+ "CUSTOM_LISTS_VALIDATION_ERROR_EMPTY_FIELD",
+ tableName: "CutstomLists",
+ value: "A custom list with this name exists, please choose a unique name.",
+ comment: ""
+ )
+ }
+ }
+}
+
+extension Collection<CustomListFieldValidationError> {
+ var settingsFieldValidationErrors: [SettingsFieldValidationError] {
+ map { SettingsFieldValidationError(errorDescription: $0.errorDescription) }
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/CustomListViewController.swift b/ios/MullvadVPN/Coordinators/CustomLists/CustomListViewController.swift
new file mode 100644
index 0000000000..e171bf6624
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/CustomLists/CustomListViewController.swift
@@ -0,0 +1,190 @@
+//
+// CustomListViewController.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-02-15.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Combine
+import MullvadSettings
+import UIKit
+
+protocol CustomListViewControllerDelegate: AnyObject {
+ func customListDidSave()
+ func customListDidDelete()
+ func showLocations()
+}
+
+class CustomListViewController: UIViewController {
+ typealias DataSource = UITableViewDiffableDataSource<CustomListSectionIdentifier, CustomListItemIdentifier>
+
+ private let interactor: CustomListInteractorProtocol
+ private let tableView = UITableView(frame: .zero, style: .insetGrouped)
+ private let subject: CurrentValueSubject<CustomListViewModel, Never>
+ private var cancellables = Set<AnyCancellable>()
+ private var dataSource: DataSource?
+ private let alertPresenter: AlertPresenter
+ private var validationErrors: Set<CustomListFieldValidationError> = []
+
+ private lazy var cellConfiguration: CustomListCellConfiguration = {
+ CustomListCellConfiguration(tableView: tableView, subject: subject)
+ }()
+
+ private lazy var dataSourceConfiguration: CustomListDataSourceConfiguration? = {
+ dataSource.flatMap { dataSource in
+ CustomListDataSourceConfiguration(dataSource: dataSource)
+ }
+ }()
+
+ lazy var saveBarButton: UIBarButtonItem = {
+ let barButtonItem = UIBarButtonItem(
+ title: NSLocalizedString(
+ "CUSTOM_LIST_NAVIGATION_SAVE_BUTTON",
+ tableName: "CustomLists",
+ value: "Save",
+ comment: ""
+ ),
+ primaryAction: UIAction { _ in
+ self.onSave()
+ }
+ )
+ barButtonItem.style = .done
+
+ return barButtonItem
+ }()
+
+ weak var delegate: CustomListViewControllerDelegate?
+
+ init(
+ interactor: CustomListInteractorProtocol,
+ subject: CurrentValueSubject<CustomListViewModel, Never>,
+ alertPresenter: AlertPresenter
+ ) {
+ self.subject = subject
+ self.interactor = interactor
+ self.alertPresenter = alertPresenter
+
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ view.directionalLayoutMargins = UIMetrics.contentLayoutMargins
+ view.backgroundColor = .secondaryColor
+ isModalInPresentation = true
+
+ addSubviews()
+ configureNavigationItem()
+ configureDataSource()
+ configureTableView()
+
+ subject.sink { [weak self] viewModel in
+ self?.saveBarButton.isEnabled = !viewModel.name.isEmpty
+ self?.validationErrors.removeAll()
+ }.store(in: &cancellables)
+ }
+
+ private func configureNavigationItem() {
+ navigationItem.leftBarButtonItem = UIBarButtonItem(
+ systemItem: .cancel,
+ primaryAction: UIAction(handler: { _ in
+ self.dismiss(animated: true)
+ })
+ )
+
+ navigationItem.rightBarButtonItem = saveBarButton
+ }
+
+ private func configureTableView() {
+ tableView.delegate = dataSourceConfiguration
+ tableView.backgroundColor = .secondaryColor
+ tableView.registerReusableViews(from: CustomListItemIdentifier.CellIdentifier.self)
+ }
+
+ private func configureDataSource() {
+ cellConfiguration.onDelete = {
+ self.onDelete()
+ }
+
+ dataSource = DataSource(
+ tableView: tableView,
+ cellProvider: { _, indexPath, itemIdentifier in
+ self.cellConfiguration.dequeueCell(
+ at: indexPath,
+ for: itemIdentifier,
+ validationErrors: self.validationErrors
+ )
+ }
+ )
+
+ dataSourceConfiguration?.didSelectItem = { item in
+ self.view.endEditing(false)
+
+ switch item {
+ case .name, .deleteList:
+ break
+ case .addLocations, .editLocations:
+ self.delegate?.showLocations()
+ }
+ }
+
+ dataSourceConfiguration?.updateDataSource(
+ sections: subject.value.tableSections,
+ validationErrors: validationErrors,
+ animated: false
+ )
+ }
+
+ private func addSubviews() {
+ view.addConstrainedSubviews([tableView]) {
+ tableView.pinEdgesToSuperview()
+ }
+ }
+
+ private func onSave() {
+ do {
+ try interactor.createCustomList(viewModel: subject.value)
+ delegate?.customListDidSave()
+ } catch {
+ validationErrors.insert(.name)
+ dataSourceConfiguration?.set(validationErrors: validationErrors)
+ }
+ }
+
+ private func onDelete() {
+ // TODO: Show error dialog.
+ delegate?.customListDidDelete()
+ }
+
+ private func showSaveErrorAlert() {
+ let presentation = AlertPresentation(
+ id: "api-custom-lists-save-list-alert",
+ icon: .alert,
+ message: NSLocalizedString(
+ "CUSTOM_LISTS_SAVE_ERROR_PROMPT",
+ tableName: "APIAccess",
+ value: "List name is already taken.",
+ comment: ""
+ ),
+ buttons: [
+ AlertAction(
+ title: NSLocalizedString(
+ "CUSTOM_LISTS_OK_BUTTON",
+ tableName: "APIAccess",
+ value: "Got it!",
+ comment: ""
+ ),
+ style: .default
+ ),
+ ]
+ )
+
+ alertPresenter.showAlert(presentation: presentation, animated: true)
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/CustomListViewModel.swift b/ios/MullvadVPN/Coordinators/CustomLists/CustomListViewModel.swift
new file mode 100644
index 0000000000..b41d52d2f5
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/CustomLists/CustomListViewModel.swift
@@ -0,0 +1,21 @@
+//
+// CustomListViewModel.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-02-14.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadSettings
+import MullvadTypes
+
+struct CustomListViewModel {
+ var id: UUID
+ var name: String
+ var locations: [RelayLocation]
+ let tableSections: [CustomListSectionIdentifier]
+
+ var customList: CustomList {
+ CustomList(id: id, name: name, locations: locations)
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/EditCustomListCoordinator.swift b/ios/MullvadVPN/Coordinators/CustomLists/EditCustomListCoordinator.swift
new file mode 100644
index 0000000000..2da41754b8
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/CustomLists/EditCustomListCoordinator.swift
@@ -0,0 +1,72 @@
+//
+// EditCustomListCoordinator.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-02-15.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Combine
+import MullvadSettings
+import Routing
+import UIKit
+
+class EditCustomListCoordinator: Coordinator, Presentable, Presenting {
+ let navigationController: UINavigationController
+ let customListInteractor: CustomListInteractorProtocol
+
+ var presentedViewController: UIViewController {
+ navigationController
+ }
+
+ var didFinish: (() -> Void)?
+
+ init(
+ navigationController: UINavigationController,
+ customListInteractor: CustomListInteractorProtocol
+ ) {
+ self.navigationController = navigationController
+ self.customListInteractor = customListInteractor
+ }
+
+ func start() {
+ let subject = CurrentValueSubject<CustomListViewModel, Never>(
+ CustomListViewModel(
+ id: UUID(),
+ name: "A list",
+ locations: [],
+ tableSections: [.name, .editLocations, .deleteList]
+ )
+ )
+
+ let controller = CustomListViewController(
+ interactor: customListInteractor,
+ subject: subject,
+ alertPresenter: AlertPresenter(context: self)
+ )
+ controller.delegate = self
+
+ controller.navigationItem.title = NSLocalizedString(
+ "CUSTOM_LIST_NAVIGATION_TITLE",
+ tableName: "CustomLists",
+ value: subject.value.name,
+ comment: ""
+ )
+
+ navigationController.pushViewController(controller, animated: false)
+ }
+}
+
+extension EditCustomListCoordinator: CustomListViewControllerDelegate {
+ func customListDidSave() {
+ didFinish?()
+ }
+
+ func customListDidDelete() {
+ didFinish?()
+ }
+
+ func showLocations() {
+ // TODO: Show view controller for locations.
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/SelectLocationCoordinator.swift b/ios/MullvadVPN/Coordinators/SelectLocationCoordinator.swift
index 67e892e3af..936c6ce4aa 100644
--- a/ios/MullvadVPN/Coordinators/SelectLocationCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/SelectLocationCoordinator.swift
@@ -11,6 +11,8 @@ import MullvadTypes
import Routing
import UIKit
+import MullvadSettings
+
class SelectLocationCoordinator: Coordinator, Presentable, Presenting, RelayCacheTrackerObserver {
private let tunnelManager: TunnelManager
private let relayCacheTracker: RelayCacheTracker
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsCellConfiguration.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsCellConfiguration.swift
index c9b69ffd20..ac712ef6fa 100644
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsCellConfiguration.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsCellConfiguration.swift
@@ -134,8 +134,8 @@ class MethodSettingsCellConfiguration {
itemIdentifier: MethodSettingsItemIdentifier,
contentValidationErrors: [AccessMethodFieldValidationError]
) {
- var contentConfiguration = MethodSettingsValidationErrorContentConfiguration()
- contentConfiguration.fieldErrors = contentValidationErrors
+ var contentConfiguration = SettingsFieldValidationErrorConfiguration()
+ contentConfiguration.errors = contentValidationErrors.settingsFieldValidationErrors
cell.contentConfiguration = contentConfiguration
}
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsValidationErrorContentConfiguration.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsValidationErrorContentConfiguration.swift
deleted file mode 100644
index 74aa8f87a4..0000000000
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsValidationErrorContentConfiguration.swift
+++ /dev/null
@@ -1,26 +0,0 @@
-//
-// MethodSettingsValidationErrorContentConfiguration.swift
-// MullvadVPN
-//
-// Created by Jon Petersson on 2024-01-12.
-// Copyright © 2024 Mullvad VPN AB. All rights reserved.
-//
-
-import UIKit
-
-/// Content configuration for presenting the access method testing progress.
-struct MethodSettingsValidationErrorContentConfiguration: UIContentConfiguration, Equatable {
- /// Field validation errors.
- var fieldErrors: [AccessMethodFieldValidationError] = []
-
- /// Layout margins.
- var directionalLayoutMargins: NSDirectionalEdgeInsets = UIMetrics.SettingsCell.apiAccessInsetLayoutMargins
-
- func makeContentView() -> UIView & UIContentView {
- return MethodSettingsValidationErrorContentView(configuration: self)
- }
-
- func updated(for state: UIConfigurationState) -> Self {
- return self
- }
-}
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodValidationError.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodValidationError.swift
index d2b41f095e..6bd2bd3274 100644
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodValidationError.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodValidationError.swift
@@ -94,3 +94,9 @@ struct AccessMethodFieldValidationError: LocalizedError, Equatable {
}
}
}
+
+extension Collection<AccessMethodFieldValidationError> {
+ var settingsFieldValidationErrors: [SettingsFieldValidationError] {
+ map { SettingsFieldValidationError(errorDescription: $0.errorDescription) }
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/Settings/SettingsFieldValidationErrorConfiguration.swift b/ios/MullvadVPN/Coordinators/Settings/SettingsFieldValidationErrorConfiguration.swift
new file mode 100644
index 0000000000..2e3ff5fbff
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/Settings/SettingsFieldValidationErrorConfiguration.swift
@@ -0,0 +1,26 @@
+//
+// SettingsFieldValidationErrorConfiguration.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-02-16.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+struct SettingsFieldValidationError: LocalizedError, Equatable {
+ var errorDescription: String?
+}
+
+struct SettingsFieldValidationErrorConfiguration: UIContentConfiguration, Equatable {
+ var errors: [SettingsFieldValidationError] = []
+ var directionalLayoutMargins: NSDirectionalEdgeInsets = UIMetrics.SettingsCell.settingsValidationErrorLayoutMargins
+
+ func makeContentView() -> UIView & UIContentView {
+ return SettingsFieldValidationErrorContentView(configuration: self)
+ }
+
+ func updated(for state: UIConfigurationState) -> Self {
+ return self
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsValidationErrorContentView.swift b/ios/MullvadVPN/Coordinators/Settings/SettingsFieldValidationErrorContentView.swift
index 38b3f0cd3a..1c10dec681 100644
--- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsValidationErrorContentView.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/SettingsFieldValidationErrorContentView.swift
@@ -1,15 +1,14 @@
//
-// MethodSettingsValidationErrorContentView.swift
+// SettingsFieldValidationErrorContentView.swift
// MullvadVPN
//
-// Created by Jon Petersson on 2024-01-12.
+// Created by Jon Petersson on 2024-02-16.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//
import UIKit
-/// Content view presenting the access method validation errors.
-class MethodSettingsValidationErrorContentView: UIView, UIContentView {
+class SettingsFieldValidationErrorContentView: UIView, UIContentView {
let contentView = UIStackView()
var icon: UIImageView {
@@ -24,7 +23,7 @@ class MethodSettingsValidationErrorContentView: UIView, UIContentView {
actualConfiguration
}
set {
- guard let newConfiguration = newValue as? MethodSettingsValidationErrorContentConfiguration else { return }
+ guard let newConfiguration = newValue as? SettingsFieldValidationErrorConfiguration else { return }
let previousConfiguration = actualConfiguration
actualConfiguration = newConfiguration
@@ -33,13 +32,13 @@ class MethodSettingsValidationErrorContentView: UIView, UIContentView {
}
}
- private var actualConfiguration: MethodSettingsValidationErrorContentConfiguration
+ private var actualConfiguration: SettingsFieldValidationErrorConfiguration
func supports(_ configuration: UIContentConfiguration) -> Bool {
- configuration is MethodSettingsValidationErrorContentConfiguration
+ configuration is SettingsFieldValidationErrorConfiguration
}
- init(configuration: MethodSettingsValidationErrorContentConfiguration) {
+ init(configuration: SettingsFieldValidationErrorConfiguration) {
actualConfiguration = configuration
super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 44))
@@ -61,7 +60,7 @@ class MethodSettingsValidationErrorContentView: UIView, UIContentView {
}
}
- private func configureSubviews(previousConfiguration: MethodSettingsValidationErrorContentConfiguration? = nil) {
+ private func configureSubviews(previousConfiguration: SettingsFieldValidationErrorConfiguration? = nil) {
guard actualConfiguration != previousConfiguration else { return }
configureLayoutMargins()
@@ -70,7 +69,7 @@ class MethodSettingsValidationErrorContentView: UIView, UIContentView {
view.removeFromSuperview()
}
- actualConfiguration.fieldErrors.forEach { error in
+ actualConfiguration.errors.forEach { error in
let label = UILabel()
label.text = error.errorDescription
label.numberOfLines = 0
diff --git a/ios/MullvadVPN/UI appearance/UIMetrics.swift b/ios/MullvadVPN/UI appearance/UIMetrics.swift
index 0e6ea7d00c..3459c5a787 100644
--- a/ios/MullvadVPN/UI appearance/UIMetrics.swift
+++ b/ios/MullvadVPN/UI appearance/UIMetrics.swift
@@ -83,7 +83,14 @@ enum UIMetrics {
static let apiAccessLayoutMargins = NSDirectionalEdgeInsets(top: 20, leading: 16, bottom: 20, trailing: 16)
static let apiAccessInsetLayoutMargins = NSDirectionalEdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)
+ static let settingsValidationErrorLayoutMargins = NSDirectionalEdgeInsets(
+ top: 8,
+ leading: 16,
+ bottom: 8,
+ trailing: 16
+ )
static let apiAccessCellHeight: CGFloat = 44
+ static let customListsCellHeight: CGFloat = 44
static let apiAccessSwitchCellTrailingMargin: CGFloat = apiAccessInsetLayoutMargins.trailing - 4
static let apiAccessPickerListContentInsetTop: CGFloat = 16
}
diff --git a/ios/MullvadVPNTests/CustomListRepositoryTests.swift b/ios/MullvadVPNTests/CustomListRepositoryTests.swift
index d7b80a6374..be7bffc24e 100644
--- a/ios/MullvadVPNTests/CustomListRepositoryTests.swift
+++ b/ios/MullvadVPNTests/CustomListRepositoryTests.swift
@@ -30,8 +30,9 @@ class CustomListRepositoryTests: XCTestCase {
func testFailedAddingDuplicateCustomList() throws {
let name = "Netflix"
- let item = try XCTUnwrap(repository.create(name))
- XCTAssertThrowsError(try repository.create(item.name)) { error in
+ let item = try XCTUnwrap(repository.create(name, locations: []))
+
+ XCTAssertThrowsError(try repository.create(item.name, locations: [])) { error in
XCTAssertEqual(error as? CustomRelayListError, CustomRelayListError.duplicateName)
}
}
@@ -39,11 +40,10 @@ class CustomListRepositoryTests: XCTestCase {
func testAddingCustomList() throws {
let name = "Netflix"
- var item = try XCTUnwrap(repository.create(name))
- item.list.append(.country("SE"))
- item.list.append(.city("SE", "Gothenburg"))
-
- repository.update(item)
+ let item = try XCTUnwrap(repository.create(name, locations: [
+ .country("SE"),
+ .city("SE", "Gothenburg"),
+ ]))
let storedItem = repository.fetch(by: item.id)
XCTAssertEqual(storedItem, item)
@@ -52,10 +52,10 @@ class CustomListRepositoryTests: XCTestCase {
func testDeletingCustomList() throws {
let name = "Netflix"
- var item = try XCTUnwrap(repository.create(name))
- item.list.append(.country("SE"))
- item.list.append(.city("SE", "Gothenburg"))
- repository.update(item)
+ let item = try XCTUnwrap(repository.create(name, locations: [
+ .country("SE"),
+ .city("SE", "Gothenburg"),
+ ]))
let storedItem = repository.fetch(by: item.id)
repository.delete(id: try XCTUnwrap(storedItem?.id))
@@ -64,15 +64,15 @@ class CustomListRepositoryTests: XCTestCase {
}
func testFetchingAllCustomList() throws {
- var streaming = try XCTUnwrap(repository.create("Netflix"))
- streaming.list.append(.country("FR"))
- streaming.list.append(.city("SE", "Gothenburg"))
- repository.update(streaming)
+ _ = try XCTUnwrap(repository.create("Netflix", locations: [
+ .country("FR"),
+ .city("SE", "Gothenburg"),
+ ]))
- var gaming = try XCTUnwrap(repository.create("PS5"))
- gaming.list.append(.country("DE"))
- gaming.list.append(.city("SE", "Gothenburg"))
- repository.update(streaming)
+ _ = try XCTUnwrap(repository.create("PS5", locations: [
+ .country("DE"),
+ .city("SE", "Gothenburg"),
+ ]))
XCTAssertEqual(repository.fetchAll().count, 2)
}