summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authormojganii <mojgan.jelodar@codic.se>2024-03-22 15:33:17 +0100
committerBug Magnet <marco.nikic@mullvad.net>2024-04-05 09:34:32 +0200
commitcf85ff5daf1c656e17fc237464ccbc843ad6e136 (patch)
treeafaacc93f1f405ada2c7c2042f01640d94bc6f40
parent711ffc84de5f3f88ca9cc4344494d1c47e39d0d6 (diff)
downloadmullvadvpn-cf85ff5daf1c656e17fc237464ccbc843ad6e136.tar.xz
mullvadvpn-cf85ff5daf1c656e17fc237464ccbc843ad6e136.zip
Add locations to a custom list
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj16
-rw-r--r--ios/MullvadVPN/Classes/AccessbilityIdentifier.swift1
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/AddCustomListCoordinator.swift46
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/AddLocationsCoordinator.swift63
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/AddLocationsDataSource.swift318
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift108
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/CustomListViewController.swift20
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/EditCustomListCoordinator.swift50
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/EditLocationsCoordinator.swift61
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/ListCustomListCoordinator.swift33
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/ListCustomListViewController.swift23
-rw-r--r--ios/MullvadVPN/Coordinators/LocationCoordinator.swift44
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/CustomListsDataSource.swift28
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/InMemoryCustomListRepository.swift2
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/LocationNode.swift4
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift8
16 files changed, 719 insertions, 106 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index f14b01f8ca..e56b72cf8e 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -818,6 +818,7 @@
E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */; };
E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E158B35F285381C60002F069 /* String+AccountFormatting.swift */; };
E1FD0DF528AA7CE400299DB4 /* StatusActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */; };
+ F006CCFC2B99CC8400C6C2AC /* EditLocationsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F006CCFB2B99CC8400C6C2AC /* EditLocationsCoordinator.swift */; };
F0164EBA2B4456D30020268D /* AccessMethodRepositoryStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EB92B4456D30020268D /* AccessMethodRepositoryStub.swift */; };
F0164EBC2B482E430020268D /* AppStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EBB2B482E430020268D /* AppStorage.swift */; };
F0164EBE2B4BFF940020268D /* ShadowsocksLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EBD2B4BFF940020268D /* ShadowsocksLoader.swift */; };
@@ -825,6 +826,9 @@
F0164ED12B4F2DCB0020268D /* AccessMethodIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164ED02B4F2DCB0020268D /* AccessMethodIterator.swift */; };
F028A56A2A34D4E700C0CAA3 /* RedeemVoucherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A5692A34D4E700C0CAA3 /* RedeemVoucherViewController.swift */; };
F028A56C2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A56B2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift */; };
+ F02F41A02B9723AF00625A4F /* AddLocationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02F419A2B9723AE00625A4F /* AddLocationsViewController.swift */; };
+ F02F41A12B9723AF00625A4F /* AddLocationsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02F419B2B9723AE00625A4F /* AddLocationsDataSource.swift */; };
+ F02F41A22B9723AF00625A4F /* AddLocationsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02F419C2B9723AF00625A4F /* AddLocationsCoordinator.swift */; };
F02F41A42B9723AF00625A4F /* AddLocationCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02F419E2B9723AF00625A4F /* AddLocationCellViewModel.swift */; };
F02F41A52B9723AF00625A4F /* AddLocationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02F419F2B9723AF00625A4F /* AddLocationCell.swift */; };
F03580252A13842C00E5DAFD /* IncreasedHitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */; };
@@ -1976,6 +1980,7 @@
E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeContentView.swift; sourceTree = "<group>"; };
E158B35F285381C60002F069 /* String+AccountFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+AccountFormatting.swift"; sourceTree = "<group>"; };
E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityView.swift; sourceTree = "<group>"; };
+ F006CCFB2B99CC8400C6C2AC /* EditLocationsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditLocationsCoordinator.swift; sourceTree = "<group>"; };
F0164EB92B4456D30020268D /* AccessMethodRepositoryStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodRepositoryStub.swift; sourceTree = "<group>"; };
F0164EBB2B482E430020268D /* AppStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorage.swift; sourceTree = "<group>"; };
F0164EBD2B4BFF940020268D /* ShadowsocksLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksLoader.swift; sourceTree = "<group>"; };
@@ -1983,6 +1988,9 @@
F0164ED02B4F2DCB0020268D /* AccessMethodIterator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodIterator.swift; sourceTree = "<group>"; };
F028A5692A34D4E700C0CAA3 /* RedeemVoucherViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherViewController.swift; sourceTree = "<group>"; };
F028A56B2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddCreditSucceededViewController.swift; sourceTree = "<group>"; };
+ F02F419A2B9723AE00625A4F /* AddLocationsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddLocationsViewController.swift; sourceTree = "<group>"; };
+ F02F419B2B9723AE00625A4F /* AddLocationsDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddLocationsDataSource.swift; sourceTree = "<group>"; };
+ F02F419C2B9723AF00625A4F /* AddLocationsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddLocationsCoordinator.swift; sourceTree = "<group>"; };
F02F419E2B9723AF00625A4F /* AddLocationCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddLocationCellViewModel.swift; sourceTree = "<group>"; };
F02F419F2B9723AF00625A4F /* AddLocationCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddLocationCell.swift; sourceTree = "<group>"; };
F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncreasedHitButton.swift; sourceTree = "<group>"; };
@@ -3514,6 +3522,9 @@
7A6389D72B7E3BD6008E77E1 /* AddCustomListCoordinator.swift */,
F02F419F2B9723AF00625A4F /* AddLocationCell.swift */,
F02F419E2B9723AF00625A4F /* AddLocationCellViewModel.swift */,
+ F02F419C2B9723AF00625A4F /* AddLocationsCoordinator.swift */,
+ F02F419B2B9723AE00625A4F /* AddLocationsDataSource.swift */,
+ F02F419A2B9723AE00625A4F /* AddLocationsViewController.swift */,
7A6389D22B7E3BD6008E77E1 /* CustomListCellConfiguration.swift */,
7A6389D42B7E3BD6008E77E1 /* CustomListDataSourceConfiguration.swift */,
7A6389DA2B7E3BD6008E77E1 /* CustomListInteractor.swift */,
@@ -3523,6 +3534,7 @@
7A6389E62B7E42BE008E77E1 /* CustomListViewController.swift */,
7A6389D32B7E3BD6008E77E1 /* CustomListViewModel.swift */,
7A6389E42B7E4247008E77E1 /* EditCustomListCoordinator.swift */,
+ F006CCFB2B99CC8400C6C2AC /* EditLocationsCoordinator.swift */,
7AB2B66F2BA1EB8C00B03E3B /* ListCustomListCoordinator.swift */,
7AB2B66E2BA1EB8C00B03E3B /* ListCustomListViewController.swift */,
);
@@ -5236,6 +5248,7 @@
58EE2E3B272FF814003BFF93 /* SettingsDataSourceDelegate.swift in Sources */,
5823FA5426CE49F700283BF8 /* TunnelObserver.swift in Sources */,
5888AD87227B17950051EB06 /* LocationViewController.swift in Sources */,
+ F006CCFC2B99CC8400C6C2AC /* EditLocationsCoordinator.swift in Sources */,
58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */,
586A950E290125F3007BAF2B /* ProductsRequestOperation.swift in Sources */,
7AF9BE902A39F26000DBFEDB /* Collection+Sorting.swift in Sources */,
@@ -5313,6 +5326,7 @@
5878A26F2907E7E00096FC88 /* ProblemReportInteractor.swift in Sources */,
7AB4CCBB2B691BBB006037F5 /* IPOverrideInteractor.swift in Sources */,
7A3353912AAA014400F0A71C /* SimulatorVPNConnection.swift in Sources */,
+ F02F41A22B9723AF00625A4F /* AddLocationsCoordinator.swift in Sources */,
F028A56A2A34D4E700C0CAA3 /* RedeemVoucherViewController.swift in Sources */,
7A5869C52B5A899C00640D27 /* MethodSettingsCellConfiguration.swift in Sources */,
58E11188292FA11F009FCA84 /* SettingsMigrationUIHandler.swift in Sources */,
@@ -5447,11 +5461,13 @@
5827B0C52B14D3E800CCBBA1 /* NSDiffableDataSourceSnapshot+Reconfigure.swift in Sources */,
58A8EE5E2976DB00009C0F8D /* StorePaymentManagerError+Display.swift in Sources */,
58A8EE5A2976BFBB009C0F8D /* SKError+Localized.swift in Sources */,
+ F02F41A12B9723AF00625A4F /* AddLocationsDataSource.swift in Sources */,
58EFC76E2AFB3BDA00E9F4CB /* ListAccessMethodCoordinator.swift in Sources */,
5827B0B92B14A1C700CCBBA1 /* MethodTestingStatusCellContentConfiguration.swift in Sources */,
7A42DEC92A05164100B209BE /* SettingsInputCell.swift in Sources */,
5803B4B22940A48700C23744 /* TunnelStore.swift in Sources */,
586A950F29012BEE007BAF2B /* AddressCacheTracker.swift in Sources */,
+ F02F41A02B9723AF00625A4F /* AddLocationsViewController.swift in Sources */,
587B753D2666468F00DEF7E9 /* NotificationController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift
index 1ddd40663d..25d7d2d270 100644
--- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift
+++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift
@@ -63,6 +63,7 @@ public enum AccessibilityIdentifier: String {
// Views
case accountView
+ case addLocationsView
case alertContainerView
case alertTitle
case changeLogAlert
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/AddCustomListCoordinator.swift b/ios/MullvadVPN/Coordinators/CustomLists/AddCustomListCoordinator.swift
index 69fb742c47..bbbf45ad54 100644
--- a/ios/MullvadVPN/Coordinators/CustomLists/AddCustomListCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/CustomLists/AddCustomListCoordinator.swift
@@ -14,26 +14,28 @@ import UIKit
class AddCustomListCoordinator: Coordinator, Presentable, Presenting {
let navigationController: UINavigationController
let interactor: CustomListInteractorProtocol
+ let nodes: [LocationNode]
+ let subject = CurrentValueSubject<CustomListViewModel, Never>(
+ CustomListViewModel(id: UUID(), name: "", locations: [], tableSections: [.name, .addLocations])
+ )
var presentedViewController: UIViewController {
navigationController
}
- var didFinish: (() -> Void)?
+ var didFinish: ((AddCustomListCoordinator) -> Void)?
init(
navigationController: UINavigationController,
- interactor: CustomListInteractorProtocol
+ interactor: CustomListInteractorProtocol,
+ nodes: [LocationNode]
) {
self.navigationController = navigationController
self.interactor = interactor
+ self.nodes = nodes
}
func start() {
- let subject = CurrentValueSubject<CustomListViewModel, Never>(
- CustomListViewModel(id: UUID(), name: "", locations: [], tableSections: [.name, .addLocations])
- )
-
let controller = CustomListViewController(
interactor: interactor,
subject: subject,
@@ -57,8 +59,11 @@ class AddCustomListCoordinator: Coordinator, Presentable, Presenting {
controller.navigationItem.leftBarButtonItem = UIBarButtonItem(
systemItem: .cancel,
- primaryAction: UIAction(handler: { _ in
- self.didFinish?()
+ primaryAction: UIAction(handler: { [weak self] _ in
+ guard let self else {
+ return
+ }
+ didFinish?(self)
})
)
@@ -68,14 +73,33 @@ class AddCustomListCoordinator: Coordinator, Presentable, Presenting {
extension AddCustomListCoordinator: CustomListViewControllerDelegate {
func customListDidSave(_ list: CustomList) {
- didFinish?()
+ didFinish?(self)
}
func customListDidDelete(_ list: CustomList) {
// No op.
}
- func showLocations() {
- // TODO: Show view controller for locations.
+ func showLocations(_ list: CustomList) {
+ let coordinator = AddLocationsCoordinator(
+ navigationController: navigationController,
+ nodes: nodes,
+ customList: list
+ )
+
+ coordinator.didFinish = { [weak self] locationsCoordinator, customList in
+ guard let self else { return }
+ subject.send(CustomListViewModel(
+ id: customList.id,
+ name: customList.name,
+ locations: customList.locations,
+ tableSections: subject.value.tableSections
+ ))
+ locationsCoordinator.removeFromParent()
+ }
+
+ coordinator.start()
+
+ addChild(coordinator)
}
}
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsCoordinator.swift b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsCoordinator.swift
new file mode 100644
index 0000000000..d7b306d978
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsCoordinator.swift
@@ -0,0 +1,63 @@
+//
+// AddLocationsCoordinator.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2024-03-04.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Combine
+import Foundation
+import MullvadSettings
+import MullvadTypes
+import Routing
+import UIKit
+
+class AddLocationsCoordinator: Coordinator, Presentable, Presenting {
+ private let navigationController: UINavigationController
+ private let nodes: [LocationNode]
+ private var customList: CustomList
+
+ var didFinish: ((AddLocationsCoordinator, CustomList) -> Void)?
+
+ var presentedViewController: UIViewController {
+ navigationController
+ }
+
+ init(
+ navigationController: UINavigationController,
+ nodes: [LocationNode],
+ customList: CustomList
+ ) {
+ self.navigationController = navigationController
+ self.nodes = nodes
+ self.customList = customList
+ }
+
+ func start() {
+ let controller = AddLocationsViewController(
+ allLocationsNodes: nodes,
+ customList: customList
+ )
+ controller.delegate = self
+
+ controller.navigationItem.title = NSLocalizedString(
+ "ADD_LOCATIONS_NAVIGATION_TITLE",
+ tableName: "AddLocations",
+ value: "Add locations",
+ comment: ""
+ )
+
+ navigationController.pushViewController(controller, animated: true)
+ }
+}
+
+extension AddLocationsCoordinator: AddLocationsViewControllerDelegate {
+ func didUpdateSelectedLocations(locations: [RelayLocation]) {
+ customList.locations = locations
+ }
+
+ func didBack() {
+ didFinish?(self, customList)
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsDataSource.swift b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsDataSource.swift
new file mode 100644
index 0000000000..3b2125811f
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsDataSource.swift
@@ -0,0 +1,318 @@
+//
+// AddLocationsDataSource.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2024-02-29.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadSettings
+import MullvadTypes
+import UIKit
+
+enum AddLocationsSectionIdentifier: String, Hashable, CaseIterable, CellIdentifierProtocol {
+ case `default`
+
+ var cellClass: AnyClass {
+ switch self {
+ case .default: AddLocationCell.self
+ }
+ }
+}
+
+class AddLocationsDataSource: UITableViewDiffableDataSource<AddLocationsSectionIdentifier, AddLocationCellViewModel> {
+ private let tableView: UITableView
+ private let nodes: [LocationNode]
+ private var customListLocationNode: CustomListLocationNode
+ var didUpdateCustomList: ((CustomListLocationNode) -> Void)?
+
+ init(
+ tableView: UITableView,
+ allLocations: [LocationNode],
+ customList: CustomList
+ ) {
+ self.tableView = tableView
+ self.nodes = allLocations
+
+ self.customListLocationNode = CustomListLocationNodeBuilder(
+ customList: customList,
+ allLocations: self.nodes
+ ).customListLocationNode
+
+ super.init(tableView: tableView) { _, indexPath, itemIdentifier in
+ let cell = tableView.dequeueReusableView(
+ withIdentifier: AddLocationsSectionIdentifier.allCases[indexPath.section],
+ for: indexPath
+ // swiftlint:disable:next force_cast
+ ) as! AddLocationCell
+ cell.configure(item: itemIdentifier)
+ cell.selectionStyle = .none
+ return cell
+ }
+
+ tableView.delegate = self
+ tableView.registerReusableViews(from: AddLocationsSectionIdentifier.self)
+ defaultRowAnimation = .fade
+ reloadWithSelectedLocations()
+ }
+
+ private func reloadWithSelectedLocations() {
+ var locationsList: [AddLocationCellViewModel] = []
+ nodes.forEach { node in
+ let viewModel = AddLocationCellViewModel(
+ node: node,
+ isSelected: customListLocationNode.children.contains(node)
+ )
+ locationsList.append(viewModel)
+
+ // Determine if the node should be expanded.
+ guard isLocationInCustomList(node: node) else {
+ return
+ }
+
+ // Walk tree backwards to determine which nodes should be expanded.
+ node.forEachAncestor { node in
+ node.showsChildren = true
+ }
+
+ locationsList.append(contentsOf: recursivelyCreateCellViewModelTree(
+ for: node,
+ in: .default,
+ indentationLevel: 1
+ ))
+ }
+ updateDataSnapshot(with: [locationsList])
+ }
+
+ private func updateDataSnapshot(
+ with list: [[AddLocationCellViewModel]],
+ animated: Bool = false,
+ completion: (() -> Void)? = nil
+ ) {
+ var snapshot = NSDiffableDataSourceSnapshot<AddLocationsSectionIdentifier, AddLocationCellViewModel>()
+ let sections = AddLocationsSectionIdentifier.allCases
+
+ snapshot.appendSections(sections)
+
+ for (index, section) in sections.enumerated() {
+ let items = list[index]
+
+ snapshot.appendItems(items, toSection: section)
+ }
+ apply(snapshot, animatingDifferences: animated, completion: completion)
+ }
+
+ private func recursivelyCreateCellViewModelTree(
+ for node: LocationNode,
+ in section: AddLocationsSectionIdentifier,
+ indentationLevel: Int
+ ) -> [AddLocationCellViewModel] {
+ var viewModels = [AddLocationCellViewModel]()
+ for childNode in node.children {
+ viewModels.append(
+ AddLocationCellViewModel(
+ node: childNode,
+ indentationLevel: indentationLevel,
+ isSelected: customListLocationNode.children.contains(childNode)
+ )
+ )
+
+ let indentationLevel = indentationLevel + 1
+
+ // Walk tree forward to determine which nodes should be expanded.
+ if isLocationInCustomList(node: childNode) {
+ viewModels.append(
+ contentsOf: recursivelyCreateCellViewModelTree(
+ for: childNode,
+ in: section,
+ indentationLevel: indentationLevel
+ )
+ )
+ }
+ }
+
+ return viewModels
+ }
+
+ private func isLocationInCustomList(node: LocationNode) -> Bool {
+ customListLocationNode.children.contains(where: { containsChild(parent: node, child: $0) })
+ }
+
+ private func containsChild(parent: LocationNode, child: LocationNode) -> Bool {
+ parent.flattened.contains(child)
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ // swiftlint:disable:next force_cast
+ let cell = super.tableView(tableView, cellForRowAt: indexPath) as! AddLocationCell
+ cell.delegate = self
+ return cell
+ }
+}
+
+extension AddLocationsDataSource: UITableViewDelegate {
+ func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int {
+ itemIdentifier(for: indexPath)?.indentationLevel ?? 0
+ }
+}
+
+extension AddLocationsDataSource: AddLocationCellDelegate {
+ func toggleExpanding(cell: AddLocationCell) {
+ guard let indexPath = tableView.indexPath(for: cell),
+ let item = itemIdentifier(for: indexPath) else { return }
+ let isExpanded = item.node.showsChildren
+
+ item.node.showsChildren = !isExpanded
+
+ var locationList = snapshot().itemIdentifiers
+
+ if !isExpanded {
+ locationList.addSubNodes(from: item, at: indexPath)
+ } else {
+ locationList.removeSubNodes(from: item.node)
+ }
+
+ updateDataSnapshot(with: [locationList], animated: true, completion: {
+ self.scroll(to: item, animated: true)
+ })
+ }
+
+ func toggleSelection(cell: AddLocationCell) {
+ guard let index = tableView.indexPath(for: cell)?.row else { return }
+
+ var locationList = snapshot().itemIdentifiers
+ let item = locationList[index]
+ let isSelected = !item.isSelected
+ locationList[index].isSelected = isSelected
+
+ locationList.deselectAncestors(from: item.node)
+ locationList.toggleSelectionSubNodes(from: item.node, isSelected: isSelected)
+
+ if isSelected {
+ customListLocationNode.add(selectedLocation: item.node)
+ } else {
+ customListLocationNode.remove(selectedLocation: item.node, with: locationList)
+ }
+ updateDataSnapshot(with: [locationList], completion: {
+ self.didUpdateCustomList?(self.customListLocationNode)
+ })
+ }
+}
+
+extension AddLocationsDataSource {
+ private func scroll(to item: AddLocationCellViewModel, animated: Bool) {
+ guard
+ let visibleIndexPaths = tableView.indexPathsForVisibleRows,
+ let indexPath = indexPath(for: item)
+ else { return }
+
+ if item.node.children.count > visibleIndexPaths.count {
+ tableView.scrollToRow(at: indexPath, at: .top, animated: animated)
+ } else {
+ if let last = item.node.children.last {
+ if let lastInsertedIndexPath = self.indexPath(for: AddLocationCellViewModel(
+ node: last,
+ isSelected: false
+ )),
+ let lastVisibleIndexPath = visibleIndexPaths.last,
+ lastInsertedIndexPath >= lastVisibleIndexPath {
+ tableView.scrollToRow(at: lastInsertedIndexPath, at: .bottom, animated: animated)
+ }
+ }
+ }
+ }
+}
+
+// MARK: - Toggle expanding
+
+fileprivate extension [AddLocationCellViewModel] {
+ mutating func addSubNodes(from item: AddLocationCellViewModel, at indexPath: IndexPath) {
+ let row = indexPath.row + 1
+ let locations = item.node.children.map {
+ AddLocationCellViewModel(node: $0, indentationLevel: item.indentationLevel + 1, isSelected: item.isSelected)
+ }
+
+ if row < count {
+ insert(contentsOf: locations, at: row)
+ } else {
+ append(contentsOf: locations)
+ }
+ }
+
+ mutating func removeSubNodes(from node: LocationNode) {
+ for node in node.children {
+ node.showsChildren = false
+ removeAll(where: { node == $0.node })
+ removeSubNodes(from: node)
+ }
+ }
+}
+
+// MARK: - Toggle selection in table view
+
+fileprivate extension [AddLocationCellViewModel] {
+ mutating func deselectAncestors(from node: LocationNode?) {
+ node?.forEachAncestor { parent in
+ guard let index = firstIndex(where: { $0.node == parent }) else {
+ return
+ }
+ self[index].isSelected = false
+ }
+ }
+
+ mutating func toggleSelectionSubNodes(from node: LocationNode, isSelected: Bool) {
+ node.forEachDescendant { child in
+ guard let index = firstIndex(where: { $0.node == child }) else {
+ return
+ }
+ self[index].isSelected = isSelected
+ }
+ }
+}
+
+// MARK: - Update custom list
+
+fileprivate extension CustomListLocationNode {
+ func remove(selectedLocation: LocationNode, with locationList: [AddLocationCellViewModel]) {
+ if let index = children.firstIndex(of: selectedLocation) {
+ children.remove(at: index)
+ }
+ removeAncestors(node: selectedLocation)
+ addSiblings(from: locationList, for: selectedLocation)
+ }
+
+ func add(selectedLocation: LocationNode) {
+ children.append(selectedLocation)
+ removeSubNodes(node: selectedLocation)
+ }
+
+ private func removeSubNodes(node: LocationNode) {
+ node.forEachDescendant { child in
+ // removing children if they are already added to custom list
+ if let index = children.firstIndex(of: child) {
+ children.remove(at: index)
+ }
+ }
+ }
+
+ private func removeAncestors(node: LocationNode) {
+ node.forEachAncestor { parent in
+ if let index = children.firstIndex(of: parent) {
+ children.remove(at: index)
+ }
+ }
+ }
+
+ private func addSiblings(from locationList: [AddLocationCellViewModel], for node: LocationNode) {
+ guard let parent = node.parent else { return }
+ parent.children.forEach { child in
+ // adding siblings if they are already selected in snapshot
+ if let item = locationList.first(where: { $0.node == child }),
+ item.isSelected && !children.contains(child) {
+ children.append(child)
+ }
+ }
+ addSiblings(from: locationList, for: parent)
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift
new file mode 100644
index 0000000000..8fc9928d8c
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift
@@ -0,0 +1,108 @@
+//
+// AddLocationsViewController.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2024-02-29.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadSettings
+import MullvadTypes
+import UIKit
+
+protocol AddLocationsViewControllerDelegate: AnyObject {
+ func didUpdateSelectedLocations(locations: [RelayLocation])
+ func didBack()
+}
+
+class AddLocationsViewController: UIViewController {
+ private var dataSource: AddLocationsDataSource?
+ private let nodes: [LocationNode]
+ private let customList: CustomList
+
+ weak var delegate: AddLocationsViewControllerDelegate?
+ private let tableView: UITableView = {
+ let tableView = UITableView()
+ tableView.separatorColor = .secondaryColor
+ tableView.separatorInset = .zero
+ tableView.rowHeight = 56
+ tableView.indicatorStyle = .white
+ tableView.accessibilityIdentifier = .addLocationsView
+ tableView.allowsMultipleSelection = true
+ tableView.tableHeaderView = nil
+ tableView.sectionHeaderHeight = .zero
+ return tableView
+ }()
+
+ private lazy var backBarButton: UIBarButtonItem = {
+ let backBarButton = UIBarButtonItem(
+ primaryAction: UIAction(
+ image: UIImage(resource: .iconBack),
+ handler: { [weak self] _ in
+ guard let self else { return }
+ delegate?.didBack()
+ navigationController?.popViewController(animated: true)
+ }
+ )
+ )
+ backBarButton.style = .done
+
+ return backBarButton
+ }()
+
+ init(
+ allLocationsNodes: [LocationNode],
+ customList: CustomList
+ ) {
+ self.nodes = allLocationsNodes
+ self.customList = customList
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ tableView.backgroundColor = view.backgroundColor
+ view.backgroundColor = .secondaryColor
+ navigationItem.leftBarButtonItem = backBarButton
+ addConstraints()
+ setUpDataSource()
+ }
+
+ private func addConstraints() {
+ view.addConstrainedSubviews([tableView]) {
+ tableView.pinEdgesToSuperview()
+ }
+ }
+
+ private func setUpDataSource() {
+ dataSource = AddLocationsDataSource(
+ tableView: tableView,
+ allLocations: nodes.copy(),
+ customList: customList
+ )
+
+ dataSource?.didUpdateCustomList = { [weak self] customListLocationNode in
+ guard let self else { return }
+ delegate?.didUpdateSelectedLocations(
+ locations: customListLocationNode.children.reduce([]) { partialResult, locationNode in
+ partialResult + locationNode.locations
+ }
+ )
+ }
+ }
+}
+
+fileprivate extension [LocationNode] {
+ func copy() -> Self {
+ map {
+ let copy = $0.copy()
+ copy.showsChildren = false
+ copy.flattened.forEach { $0.showsChildren = false }
+ return copy
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/CustomListViewController.swift b/ios/MullvadVPN/Coordinators/CustomLists/CustomListViewController.swift
index 43ad9ed259..4e5891658d 100644
--- a/ios/MullvadVPN/Coordinators/CustomLists/CustomListViewController.swift
+++ b/ios/MullvadVPN/Coordinators/CustomLists/CustomListViewController.swift
@@ -13,7 +13,7 @@ import UIKit
protocol CustomListViewControllerDelegate: AnyObject {
func customListDidSave(_ list: CustomList)
func customListDidDelete(_ list: CustomList)
- func showLocations()
+ func showLocations(_ list: CustomList)
}
class CustomListViewController: UIViewController {
@@ -45,8 +45,8 @@ class CustomListViewController: UIViewController {
value: "Save",
comment: ""
),
- primaryAction: UIAction { _ in
- self.onSave()
+ primaryAction: UIAction { [weak self] _ in
+ self?.onSave()
}
)
barButtonItem.style = .done
@@ -101,14 +101,15 @@ class CustomListViewController: UIViewController {
}
private func configureDataSource() {
- cellConfiguration.onDelete = {
- self.onDelete()
+ cellConfiguration.onDelete = { [weak self] in
+ self?.onDelete()
}
dataSource = DataSource(
tableView: tableView,
- cellProvider: { _, indexPath, itemIdentifier in
- self.cellConfiguration.dequeueCell(
+ cellProvider: { [weak self] _, indexPath, itemIdentifier in
+ guard let self else { return nil }
+ return cellConfiguration.dequeueCell(
at: indexPath,
for: itemIdentifier,
validationErrors: self.validationErrors
@@ -116,14 +117,15 @@ class CustomListViewController: UIViewController {
}
)
- dataSourceConfiguration?.didSelectItem = { item in
+ dataSourceConfiguration?.didSelectItem = { [weak self] item in
+ guard let self else { return }
self.view.endEditing(false)
switch item {
case .name, .deleteList:
break
case .addLocations, .editLocations:
- self.delegate?.showLocations()
+ delegate?.showLocations(self.subject.value.customList)
}
}
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/EditCustomListCoordinator.swift b/ios/MullvadVPN/Coordinators/CustomLists/EditCustomListCoordinator.swift
index d8677161bc..9e753f22d6 100644
--- a/ios/MullvadVPN/Coordinators/CustomLists/EditCustomListCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/CustomLists/EditCustomListCoordinator.swift
@@ -19,33 +19,34 @@ class EditCustomListCoordinator: Coordinator, Presentable, Presenting {
let navigationController: UINavigationController
let customListInteractor: CustomListInteractorProtocol
let customList: CustomList
+ let nodes: [LocationNode]
+ let subject: CurrentValueSubject<CustomListViewModel, Never>
var presentedViewController: UIViewController {
navigationController
}
- var didFinish: ((FinishAction, CustomList) -> Void)?
+ var didFinish: ((EditCustomListCoordinator, FinishAction, CustomList) -> Void)?
init(
navigationController: UINavigationController,
customListInteractor: CustomListInteractorProtocol,
- customList: CustomList
+ customList: CustomList,
+ nodes: [LocationNode]
) {
self.navigationController = navigationController
self.customListInteractor = customListInteractor
self.customList = customList
+ self.nodes = nodes
+ self.subject = CurrentValueSubject(CustomListViewModel(
+ id: customList.id,
+ name: customList.name,
+ locations: customList.locations,
+ tableSections: [.name, .editLocations, .deleteList]
+ ))
}
func start() {
- let subject = CurrentValueSubject<CustomListViewModel, Never>(
- CustomListViewModel(
- id: customList.id,
- name: customList.name,
- locations: customList.locations,
- tableSections: [.name, .editLocations, .deleteList]
- )
- )
-
let controller = CustomListViewController(
interactor: customListInteractor,
subject: subject,
@@ -66,14 +67,33 @@ class EditCustomListCoordinator: Coordinator, Presentable, Presenting {
extension EditCustomListCoordinator: CustomListViewControllerDelegate {
func customListDidSave(_ list: CustomList) {
- didFinish?(.save, list)
+ didFinish?(self, .save, list)
}
func customListDidDelete(_ list: CustomList) {
- didFinish?(.delete, list)
+ didFinish?(self, .delete, list)
}
- func showLocations() {
- // TODO: Show view controller for locations.
+ func showLocations(_ list: CustomList) {
+ let coordinator = EditLocationsCoordinator(
+ navigationController: navigationController,
+ nodes: nodes,
+ customList: list
+ )
+
+ coordinator.didFinish = { [weak self] locationsCoordinator, customList in
+ guard let self else { return }
+ subject.send(CustomListViewModel(
+ id: customList.id,
+ name: customList.name,
+ locations: customList.locations,
+ tableSections: subject.value.tableSections
+ ))
+ locationsCoordinator.removeFromParent()
+ }
+
+ coordinator.start()
+
+ coordinator.addChild(coordinator)
}
}
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/EditLocationsCoordinator.swift b/ios/MullvadVPN/Coordinators/CustomLists/EditLocationsCoordinator.swift
new file mode 100644
index 0000000000..c9687c31ea
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/CustomLists/EditLocationsCoordinator.swift
@@ -0,0 +1,61 @@
+//
+// EditLocationsCoordinator.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2024-03-07.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadSettings
+import MullvadTypes
+import Routing
+import UIKit
+
+class EditLocationsCoordinator: Coordinator, Presentable, Presenting {
+ private let navigationController: UINavigationController
+ private let nodes: [LocationNode]
+ private var customList: CustomList
+
+ var didFinish: ((EditLocationsCoordinator, CustomList) -> Void)?
+
+ var presentedViewController: UIViewController {
+ navigationController
+ }
+
+ init(
+ navigationController: UINavigationController,
+ nodes: [LocationNode],
+ customList: CustomList
+ ) {
+ self.navigationController = navigationController
+ self.nodes = nodes
+ self.customList = customList
+ }
+
+ func start() {
+ let controller = AddLocationsViewController(
+ allLocationsNodes: nodes,
+ customList: customList
+ )
+ controller.delegate = self
+
+ controller.navigationItem.title = NSLocalizedString(
+ "EDIT_LOCATIONS_NAVIGATION_TITLE",
+ tableName: "EditLocations",
+ value: "Edit locations",
+ comment: ""
+ )
+ navigationController.pushViewController(controller, animated: true)
+ }
+}
+
+extension EditLocationsCoordinator: AddLocationsViewControllerDelegate {
+ func didUpdateSelectedLocations(locations: [RelayLocation]) {
+ customList.locations = locations
+ }
+
+ func didBack() {
+ didFinish?(self, customList)
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListCoordinator.swift b/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListCoordinator.swift
index 842d9544e6..4753c74f72 100644
--- a/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListCoordinator.swift
@@ -16,47 +16,52 @@ class ListCustomListCoordinator: Coordinator, Presentable, Presenting {
let interactor: CustomListInteractorProtocol
let tunnelManager: TunnelManager
let listViewController: ListCustomListViewController
+ let nodes: [LocationNode]
var presentedViewController: UIViewController {
navigationController
}
- var didFinish: (() -> Void)?
+ var didFinish: ((ListCustomListCoordinator) -> Void)?
init(
navigationController: UINavigationController,
interactor: CustomListInteractorProtocol,
- tunnelManager: TunnelManager
+ tunnelManager: TunnelManager,
+ nodes: [LocationNode]
) {
self.navigationController = navigationController
self.interactor = interactor
self.tunnelManager = tunnelManager
+ self.nodes = nodes
listViewController = ListCustomListViewController(interactor: interactor)
}
func start() {
- listViewController.didFinish = didFinish
- listViewController.didSelectItem = {
- self.edit(list: $0)
+ listViewController.didFinish = { [weak self] in
+ guard let self else { return }
+ didFinish?(self)
+ }
+ listViewController.didSelectItem = { [weak self] in
+ self?.edit(list: $0)
}
navigationController.pushViewController(listViewController, animated: false)
}
private func edit(list: CustomList) {
- // Remove previous edit coordinator to prevent accumulation.
- childCoordinators.filter { $0 is EditCustomListCoordinator }.forEach { $0.removeFromParent() }
-
let coordinator = EditCustomListCoordinator(
navigationController: navigationController,
customListInteractor: interactor,
- customList: list
+ customList: list,
+ nodes: nodes
)
- coordinator.didFinish = { action, list in
- self.popToList()
- coordinator.removeFromParent()
+ coordinator.didFinish = { [weak self] editCustomListCoordinator, action, list in
+ guard let self else { return }
+ popToList()
+ editCustomListCoordinator.removeFromParent()
self.updateRelayConstraints(for: action, in: list)
self.listViewController.updateDataSource(reloadExisting: action == .save)
@@ -86,8 +91,8 @@ class ListCustomListCoordinator: Coordinator, Presentable, Presenting {
relayConstraints.locations = .only(UserSelectedRelays(locations: []))
}
- tunnelManager.updateSettings([.relayConstraints(relayConstraints)]) {
- self.tunnelManager.startTunnel()
+ tunnelManager.updateSettings([.relayConstraints(relayConstraints)]) { [weak self] in
+ self?.tunnelManager.startTunnel()
}
}
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListViewController.swift b/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListViewController.swift
index 25a8e374e6..460cc5e2f7 100644
--- a/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListViewController.swift
+++ b/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListViewController.swift
@@ -22,7 +22,7 @@ private enum CellReuseIdentifier: String, CaseIterable, CellIdentifierProtocol {
var cellClass: AnyClass {
switch self {
- case .default: BasicCell.self
+ default: BasicCell.self
}
}
}
@@ -35,6 +35,21 @@ class ListCustomListViewController: UIViewController {
private var fetchedItems: [CustomList] = []
private var tableView = UITableView(frame: .zero, style: .plain)
+ private let emptyListLabel: UILabel = {
+ let textLabel = UILabel()
+ textLabel.font = .preferredFont(forTextStyle: .title2)
+ textLabel.textColor = .secondaryTextColor
+ textLabel.textAlignment = .center
+ textLabel.numberOfLines = .zero
+ textLabel.lineBreakStrategy = []
+ textLabel.text = NSLocalizedString(
+ "CustomList",
+ value: "No custom list to display",
+ comment: ""
+ )
+ return textLabel
+ }()
+
var didSelectItem: ((CustomList) -> Void)?
var didFinish: (() -> Void)?
@@ -60,7 +75,7 @@ class ListCustomListViewController: UIViewController {
func updateDataSource(reloadExisting: Bool, animated: Bool = true) {
fetchedItems = interactor.fetchAll()
-
+ tableView.backgroundView = fetchedItems.isEmpty ? emptyListLabel : nil
var snapshot = NSDiffableDataSourceSnapshot<SectionIdentifier, ItemIdentifier>()
snapshot.appendSections([.default])
@@ -87,9 +102,9 @@ class ListCustomListViewController: UIViewController {
tableView.backgroundColor = .secondaryColor
tableView.separatorColor = .secondaryColor
tableView.separatorInset = .zero
+ tableView.separatorStyle = .singleLine
tableView.contentInset.top = 16
tableView.rowHeight = UIMetrics.SettingsCell.customListsCellHeight
-
tableView.registerReusableViews(from: CellReuseIdentifier.self)
}
@@ -126,7 +141,6 @@ class ListCustomListViewController: UIViewController {
) -> UITableViewCell {
let cell = tableView.dequeueReusableView(withIdentifier: CellReuseIdentifier.default, for: indexPath)
let item = fetchedItems[indexPath.row]
-
var contentConfiguration = ListCellContentConfiguration()
contentConfiguration.text = item.name
cell.contentConfiguration = contentConfiguration
@@ -138,7 +152,6 @@ class ListCustomListViewController: UIViewController {
if let cell = cell as? CustomCellDisclosureHandling {
cell.disclosureType = .chevron
}
-
return cell
}
}
diff --git a/ios/MullvadVPN/Coordinators/LocationCoordinator.swift b/ios/MullvadVPN/Coordinators/LocationCoordinator.swift
index 8da5a6dca2..53e164274e 100644
--- a/ios/MullvadVPN/Coordinators/LocationCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/LocationCoordinator.swift
@@ -127,31 +127,35 @@ class LocationCoordinator: Coordinator, Presentable, Presenting {
return relayFilterCoordinator
}
- private func showAddCustomList() {
+ private func showAddCustomList(nodes: [LocationNode]) {
let coordinator = AddCustomListCoordinator(
navigationController: CustomNavigationController(),
- interactor: CustomListInteractor(repository: customListRepository)
+ interactor: CustomListInteractor(
+ repository: customListRepository
+ ),
+ nodes: nodes
)
- coordinator.didFinish = {
- coordinator.dismiss(animated: true)
- self.locationViewController?.refreshCustomLists()
+ coordinator.didFinish = { [weak self] addCustomListCoordinator in
+ addCustomListCoordinator.dismiss(animated: true)
+ self?.locationViewController?.refreshCustomLists()
}
coordinator.start()
presentChild(coordinator, animated: true)
}
- private func showEditCustomLists() {
+ private func showEditCustomLists(nodes: [LocationNode]) {
let coordinator = ListCustomListCoordinator(
navigationController: CustomNavigationController(),
interactor: CustomListInteractor(repository: customListRepository),
- tunnelManager: tunnelManager
+ tunnelManager: tunnelManager,
+ nodes: nodes
)
- coordinator.didFinish = {
- coordinator.dismiss(animated: true)
- self.locationViewController?.refreshCustomLists()
+ coordinator.didFinish = { [weak self] listCustomListCoordinator in
+ listCustomListCoordinator.dismiss(animated: true)
+ self?.locationViewController?.refreshCustomLists()
}
coordinator.start()
@@ -181,7 +185,7 @@ extension LocationCoordinator: RelayCacheTrackerObserver {
}
extension LocationCoordinator: LocationViewControllerDelegate {
- func didRequestRouteToCustomLists(_ controller: LocationViewController) {
+ func didRequestRouteToCustomLists(_ controller: LocationViewController, nodes: [LocationNode]) {
let actionSheet = UIAlertController(
title: NSLocalizedString(
"CUSTOM_LIST_ACTION_SHEET_TITLE",
@@ -190,7 +194,7 @@ extension LocationCoordinator: LocationViewControllerDelegate {
comment: ""
),
message: nil,
- preferredStyle: .actionSheet
+ preferredStyle: UIDevice.current.userInterfaceIdiom == .pad ? .alert : .actionSheet
)
actionSheet.addAction(UIAlertAction(
@@ -201,12 +205,11 @@ extension LocationCoordinator: LocationViewControllerDelegate {
comment: ""
),
style: .default,
- handler: { _ in
- self.showAddCustomList()
+ handler: { [weak self] _ in
+ self?.showAddCustomList(nodes: nodes)
}
))
-
- actionSheet.addAction(UIAlertAction(
+ let editAction = UIAlertAction(
title: NSLocalizedString(
"CUSTOM_LIST_ACTION_SHEET_EDIT_LISTS_BUTTON",
tableName: "CustomLists",
@@ -214,10 +217,13 @@ extension LocationCoordinator: LocationViewControllerDelegate {
comment: ""
),
style: .default,
- handler: { _ in
- self.showEditCustomLists()
+ handler: { [weak self] _ in
+ self?.showEditCustomLists(nodes: nodes)
}
- ))
+ )
+ editAction.isEnabled = !customListRepository.fetchAll().isEmpty
+
+ actionSheet.addAction(editAction)
actionSheet.addAction(UIAlertAction(
title: NSLocalizedString(
diff --git a/ios/MullvadVPN/View controllers/SelectLocation/CustomListsDataSource.swift b/ios/MullvadVPN/View controllers/SelectLocation/CustomListsDataSource.swift
index dd041642cd..e9cad7cf1c 100644
--- a/ios/MullvadVPN/View controllers/SelectLocation/CustomListsDataSource.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/CustomListsDataSource.swift
@@ -74,32 +74,4 @@ class CustomListsDataSource: LocationDataSourceProtocol {
func customList(by id: UUID) -> CustomList? {
repository.fetch(by: id)
}
-
- private func copy(
- _ location: RelayLocation,
- from allLocationNodes: [LocationNode],
- withParent parentNode: LocationNode
- ) -> LocationNode? {
- let rootNode = RootLocationNode(children: allLocationNodes)
-
- return switch location {
- case let .country(countryCode):
- rootNode
- .countryFor(code: countryCode)?
- .copy(withParent: parentNode)
-
- case let .city(countryCode, cityCode):
- rootNode
- .countryFor(code: countryCode)?
- .cityFor(codes: [countryCode, cityCode])?
- .copy(withParent: parentNode)
-
- case let .hostname(countryCode, cityCode, hostCode):
- rootNode
- .countryFor(code: countryCode)?
- .cityFor(codes: [countryCode, cityCode])?
- .hostFor(code: hostCode)?
- .copy(withParent: parentNode)
- }
- }
}
diff --git a/ios/MullvadVPN/View controllers/SelectLocation/InMemoryCustomListRepository.swift b/ios/MullvadVPN/View controllers/SelectLocation/InMemoryCustomListRepository.swift
index 7123e19a24..600692b8d5 100644
--- a/ios/MullvadVPN/View controllers/SelectLocation/InMemoryCustomListRepository.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/InMemoryCustomListRepository.swift
@@ -29,7 +29,7 @@ class InMemoryCustomListRepository: CustomListRepositoryProtocol {
),
]
- func save(list: MullvadSettings.CustomList) throws {
+ func save(list: CustomList) throws {
if let index = customRelayLists.firstIndex(where: { $0.id == list.id }) {
customRelayLists[index] = list
} else if customRelayLists.contains(where: { $0.name == list.name }) {
diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationNode.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationNode.swift
index ed639cc219..ac9277d63a 100644
--- a/ios/MullvadVPN/View controllers/SelectLocation/LocationNode.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationNode.swift
@@ -77,6 +77,10 @@ extension LocationNode {
static func combineNodeCodes(_ codes: [String]) -> String {
codes.joined(separator: "-")
}
+
+ var flattened: [LocationNode] {
+ children + children.flatMap { $0.flattened }
+ }
}
extension LocationNode {
diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift
index 2b3a1f8c15..6b27418aa5 100644
--- a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift
@@ -13,7 +13,7 @@ import MullvadTypes
import UIKit
protocol LocationViewControllerDelegate: AnyObject {
- func didRequestRouteToCustomLists(_ controller: LocationViewController)
+ func didRequestRouteToCustomLists(_ controller: LocationViewController, nodes: [LocationNode])
}
final class LocationViewController: UIViewController {
@@ -139,7 +139,7 @@ final class LocationViewController: UIViewController {
dataSource?.didTapEditCustomLists = { [weak self] in
guard let self else { return }
- delegate?.didRequestRouteToCustomLists(self)
+ delegate?.didRequestRouteToCustomLists(self, nodes: allLocationDataSource.nodes)
}
if let cachedRelays {
@@ -151,11 +151,11 @@ final class LocationViewController: UIViewController {
tableView.backgroundColor = view.backgroundColor
tableView.separatorColor = .secondaryColor
tableView.separatorInset = .zero
- tableView.estimatedRowHeight = 53
+ tableView.rowHeight = 56
+ tableView.sectionHeaderHeight = 56
tableView.indicatorStyle = .white
tableView.keyboardDismissMode = .onDrag
tableView.accessibilityIdentifier = .selectLocationTableView
- tableView.sectionHeaderHeight = 56.0
}
private func setUpTopContent() {