Skip to content

Commit 2adee19

Browse files
committed
Merge branch 'refactoring-select-location-list-view-ios-483'
2 parents e204cc7 + aba9565 commit 2adee19

12 files changed

+603
-402
lines changed

ios/MullvadTypes/RelayLocation.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public enum RelayLocation: Codable, Hashable, CustomDebugStringConvertible {
6363
}
6464

6565
/// A list of `RelayLocation` items preceding the given one in the relay tree
66-
public var ascendants: [RelayLocation] {
66+
public var ancestors: [RelayLocation] {
6767
switch self {
6868
case let .hostname(country, city, _):
6969
return [.country(country), .city(country, city)]

ios/MullvadVPN.xcodeproj/project.pbxproj

+28
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,13 @@
789789
F050AE582B7376C6003F4EDB /* CustomListRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE562B7376C6003F4EDB /* CustomListRepository.swift */; };
790790
F050AE5A2B7376F4003F4EDB /* CustomList.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE592B7376F4003F4EDB /* CustomList.swift */; };
791791
F050AE5C2B73797D003F4EDB /* CustomListRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE5B2B73797D003F4EDB /* CustomListRepositoryTests.swift */; };
792+
F050AE4C2B70D5A7003F4EDB /* SelectLocationNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE4B2B70D5A7003F4EDB /* SelectLocationNode.swift */; };
793+
F050AE4E2B70D7F8003F4EDB /* LocationCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE4D2B70D7F8003F4EDB /* LocationCellViewModel.swift */; };
794+
F050AE502B70DC4F003F4EDB /* SelectLocationNodeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE4F2B70DC4F003F4EDB /* SelectLocationNodeProtocol.swift */; };
795+
F050AE522B70DFC0003F4EDB /* SelectLocationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE512B70DFC0003F4EDB /* SelectLocationSection.swift */; };
796+
F050AE5E2B739A73003F4EDB /* LocationDataSourceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE5D2B739A73003F4EDB /* LocationDataSourceProtocol.swift */; };
797+
F050AE602B73A41E003F4EDB /* AllLocationDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE5F2B73A41E003F4EDB /* AllLocationDataSource.swift */; };
798+
F050AE622B74DBAC003F4EDB /* CustomListsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE612B74DBAC003F4EDB /* CustomListsDataSource.swift */; };
792799
F05F39942B21C6C6006E60A7 /* relays.json in Resources */ = {isa = PBXBuildFile; fileRef = 58F3C0A524A50155003E76BE /* relays.json */; };
793800
F05F39972B21C735006E60A7 /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675A26E6576800655B05 /* RelayCache.swift */; };
794801
F05F39982B21C73C006E60A7 /* CachedRelays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87626B024A600B8C587 /* CachedRelays.swift */; };
@@ -1898,6 +1905,13 @@
18981905
F050AE562B7376C6003F4EDB /* CustomListRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListRepository.swift; sourceTree = "<group>"; };
18991906
F050AE592B7376F4003F4EDB /* CustomList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomList.swift; sourceTree = "<group>"; };
19001907
F050AE5B2B73797D003F4EDB /* CustomListRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListRepositoryTests.swift; sourceTree = "<group>"; };
1908+
F050AE4B2B70D5A7003F4EDB /* SelectLocationNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationNode.swift; sourceTree = "<group>"; };
1909+
F050AE4D2B70D7F8003F4EDB /* LocationCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationCellViewModel.swift; sourceTree = "<group>"; };
1910+
F050AE4F2B70DC4F003F4EDB /* SelectLocationNodeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationNodeProtocol.swift; sourceTree = "<group>"; };
1911+
F050AE512B70DFC0003F4EDB /* SelectLocationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationSection.swift; sourceTree = "<group>"; };
1912+
F050AE5D2B739A73003F4EDB /* LocationDataSourceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationDataSourceProtocol.swift; sourceTree = "<group>"; };
1913+
F050AE5F2B73A41E003F4EDB /* AllLocationDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllLocationDataSource.swift; sourceTree = "<group>"; };
1914+
F050AE612B74DBAC003F4EDB /* CustomListsDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListsDataSource.swift; sourceTree = "<group>"; };
19011915
F06045E52B231EB700B2D37A /* URLSessionTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTransport.swift; sourceTree = "<group>"; };
19021916
F06045E92B23217E00B2D37A /* ShadowsocksTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksTransport.swift; sourceTree = "<group>"; };
19031917
F06045EB2B2322A500B2D37A /* Jittered.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Jittered.swift; sourceTree = "<group>"; };
@@ -2324,9 +2338,16 @@
23242338
583FE01729C196F3006E85F9 /* SelectLocation */ = {
23252339
isa = PBXGroup;
23262340
children = (
2341+
F050AE5F2B73A41E003F4EDB /* AllLocationDataSource.swift */,
2342+
F050AE612B74DBAC003F4EDB /* CustomListsDataSource.swift */,
23272343
58435AC129CB2A350099C71B /* LocationCellFactory.swift */,
2344+
F050AE4D2B70D7F8003F4EDB /* LocationCellViewModel.swift */,
23282345
583DA21325FA4B5C00318683 /* LocationDataSource.swift */,
2346+
F050AE5D2B739A73003F4EDB /* LocationDataSourceProtocol.swift */,
23292347
5888AD82227B11080051EB06 /* SelectLocationCell.swift */,
2348+
F050AE512B70DFC0003F4EDB /* SelectLocationSection.swift */,
2349+
F050AE4B2B70D5A7003F4EDB /* SelectLocationNode.swift */,
2350+
F050AE4F2B70DC4F003F4EDB /* SelectLocationNodeProtocol.swift */,
23302351
5888AD86227B17950051EB06 /* SelectLocationViewController.swift */,
23312352
);
23322353
path = SelectLocation;
@@ -4995,6 +5016,7 @@
49955016
58968FAE28743E2000B799DC /* TunnelInteractor.swift in Sources */,
49965017
7A1A26472A29CF0800B978AA /* RelayFilterDataSource.swift in Sources */,
49975018
5864AF0929C78850005B0CD9 /* PreferencesCellFactory.swift in Sources */,
5019+
F050AE4E2B70D7F8003F4EDB /* LocationCellViewModel.swift in Sources */,
49985020
58CEB30C2AFD586600E6E088 /* DynamicBackgroundConfiguration.swift in Sources */,
49995021
587B7536266528A200DEF7E9 /* NotificationManager.swift in Sources */,
50005022
5820EDA9288FE064006BF4E4 /* DeviceManagementInteractor.swift in Sources */,
@@ -5008,6 +5030,7 @@
50085030
5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */,
50095031
F0E8E4C52A60499100ED26A3 /* AccountDeletionViewController.swift in Sources */,
50105032
7A9CCCC12A96302800DD6A34 /* AccountCoordinator.swift in Sources */,
5033+
F050AE502B70DC4F003F4EDB /* SelectLocationNodeProtocol.swift in Sources */,
50115034
58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */,
50125035
5846227326E22A160035F7C2 /* StorePaymentObserver.swift in Sources */,
50135036
F0E3618B2A4ADD2F00AEEF2B /* WelcomeContentView.swift in Sources */,
@@ -5049,6 +5072,7 @@
50495072
7A9CCCBE2A96302800DD6A34 /* AccountDeletionCoordinator.swift in Sources */,
50505073
588527B4276B4F2F00BAA373 /* SetAccountOperation.swift in Sources */,
50515074
58FF9FE02B075ABC00E4C97D /* EditAccessMethodViewController.swift in Sources */,
5075+
F050AE622B74DBAC003F4EDB /* CustomListsDataSource.swift in Sources */,
50525076
F0DA87472A9CB9A2006044F1 /* AccountExpiryRow.swift in Sources */,
50535077
585CA70F25F8C44600B47C62 /* UIMetrics.swift in Sources */,
50545078
E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */,
@@ -5081,6 +5105,7 @@
50815105
063F026628FFE11C001FA09F /* RESTCreateApplePaymentResponse+Localization.swift in Sources */,
50825106
58DF28A52417CB4B00E836B0 /* StorePaymentManager.swift in Sources */,
50835107
583DA21425FA4B5C00318683 /* LocationDataSource.swift in Sources */,
5108+
F050AE602B73A41E003F4EDB /* AllLocationDataSource.swift in Sources */,
50845109
587EB6742714520600123C75 /* PreferencesDataSourceDelegate.swift in Sources */,
50855110
582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */,
50865111
7AF9BE8E2A331C7B00DBFEDB /* RelayFilterViewModel.swift in Sources */,
@@ -5186,7 +5211,9 @@
51865211
584592612639B4A200EF967F /* TermsOfServiceContentView.swift in Sources */,
51875212
5875960A26F371FC00BF6711 /* Tunnel+Messaging.swift in Sources */,
51885213
586C0D912B03D8A400E7CDD7 /* AccessMethodHeaderFooterReuseIdentifier.swift in Sources */,
5214+
F050AE4C2B70D5A7003F4EDB /* SelectLocationNode.swift in Sources */,
51895215
7A2960F62A963F7500389B82 /* AlertCoordinator.swift in Sources */,
5216+
F050AE522B70DFC0003F4EDB /* SelectLocationSection.swift in Sources */,
51905217
063687BA28EB234F00BE7161 /* PacketTunnelTransport.swift in Sources */,
51915218
A9C342C12ACC37E30045F00E /* TunnelStatusBlockObserver.swift in Sources */,
51925219
587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */,
@@ -5206,6 +5233,7 @@
52065233
5827B0BF2B14B37D00CCBBA1 /* Publisher+PreviousValue.swift in Sources */,
52075234
7A9CCCB62A96302800DD6A34 /* OutOfTimeCoordinator.swift in Sources */,
52085235
5827B0AA2B0F4C9100CCBBA1 /* EditAccessMethodViewControllerDelegate.swift in Sources */,
5236+
F050AE5E2B739A73003F4EDB /* LocationDataSourceProtocol.swift in Sources */,
52095237
7A5869A82B5140C200640D27 /* MethodSettingsValidationErrorContentView.swift in Sources */,
52105238
A99E5EE22B762ED30033F241 /* ProblemReportViewController+ViewManagement.swift in Sources */,
52115239
7A5869A22B502EA800640D27 /* MethodSettingsSectionIdentifier.swift in Sources */,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
//
2+
// AllLocationDataSource.swift
3+
// MullvadVPN
4+
//
5+
// Created by Mojgan on 2024-02-07.
6+
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import MullvadREST
11+
import MullvadTypes
12+
13+
class AllLocationDataSource: LocationDataSourceProtocol {
14+
var nodeByLocation = [RelayLocation: SelectLocationNode]()
15+
private var locationList = [RelayLocation]()
16+
17+
func search(by text: String) -> [RelayLocation] {
18+
guard !text.isEmpty else {
19+
return locationList
20+
}
21+
22+
var filteredLocations: [RelayLocation] = []
23+
locationList.forEach { location in
24+
guard let countryNode = nodeByLocation[location] else { return }
25+
countryNode.showsChildren = false
26+
27+
if countryNode.displayName.fuzzyMatch(text) {
28+
filteredLocations.append(countryNode.location)
29+
}
30+
31+
countryNode.children.forEach { cityNode in
32+
cityNode.showsChildren = false
33+
34+
let relaysContainSearchString = cityNode.children
35+
.contains(where: { $0.displayName.fuzzyMatch(text) })
36+
37+
if cityNode.displayName.fuzzyMatch(text) || relaysContainSearchString {
38+
if !filteredLocations.contains(countryNode.location) {
39+
filteredLocations.append(countryNode.location)
40+
}
41+
42+
filteredLocations.append(cityNode.location)
43+
countryNode.showsChildren = true
44+
45+
if relaysContainSearchString {
46+
filteredLocations.append(contentsOf: cityNode.children.map { $0.location })
47+
cityNode.showsChildren = true
48+
}
49+
}
50+
}
51+
}
52+
53+
return filteredLocations
54+
}
55+
56+
func reload(
57+
_ response: REST.ServerRelaysResponse,
58+
relays: [REST.ServerRelay]
59+
) -> [RelayLocation] {
60+
nodeByLocation.removeAll()
61+
let rootNode = self.makeRootNode(name: SelectLocationSection.allLocations.description)
62+
63+
for relay in relays {
64+
guard case let .city(countryCode, cityCode) = RelayLocation(dashSeparatedString: relay.location),
65+
let serverLocation = response.locations[relay.location] else { continue }
66+
67+
let relayLocation = RelayLocation.hostname(countryCode, cityCode, relay.hostname)
68+
69+
for ancestorOrSelf in relayLocation.ancestors + [relayLocation] {
70+
guard !nodeByLocation.keys.contains(ancestorOrSelf) else {
71+
continue
72+
}
73+
74+
// Maintain the `showsChildren` state when transitioning between relay lists
75+
let wasShowingChildren = nodeByLocation[ancestorOrSelf]?.showsChildren ?? false
76+
77+
let node = createNode(
78+
root: rootNode,
79+
ancestorOrSelf: ancestorOrSelf,
80+
serverLocation: serverLocation,
81+
relay: relay,
82+
wasShowingChildren: wasShowingChildren
83+
)
84+
nodeByLocation[ancestorOrSelf] = node
85+
}
86+
}
87+
88+
rootNode.sortChildrenRecursive()
89+
rootNode.computeActiveChildrenRecursive()
90+
locationList = rootNode.flatRelayLocationList()
91+
return locationList
92+
}
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//
2+
// CustomListsDataSource.swift
3+
// MullvadVPN
4+
//
5+
// Created by Mojgan on 2024-02-08.
6+
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import MullvadREST
11+
import MullvadTypes
12+
13+
class CustomListsDataSource: LocationDataSourceProtocol {
14+
var nodeByLocation = [RelayLocation: SelectLocationNode]()
15+
private var locationList = [RelayLocation]()
16+
17+
func search(by text: String) -> [RelayLocation] {
18+
[]
19+
}
20+
21+
func reload(
22+
_ response: REST.ServerRelaysResponse,
23+
relays: [REST.ServerRelay]
24+
) -> [RelayLocation] {
25+
locationList
26+
}
27+
}

ios/MullvadVPN/View controllers/SelectLocation/LocationCellFactory.swift

+13-9
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,26 @@ import MullvadTypes
1010
import UIKit
1111

1212
protocol LocationCellEventHandler {
13-
func collapseCell(for item: RelayLocation)
13+
func toggleCell(for item: LocationCellViewModel)
14+
func node(for item: LocationCellViewModel) -> SelectLocationNode?
1415
}
1516

1617
final class LocationCellFactory: CellFactoryProtocol {
17-
var nodeByLocation = [RelayLocation: LocationDataSource.Node]()
1818
var delegate: LocationCellEventHandler?
1919
let tableView: UITableView
20+
let reuseIdentifier: String
2021

21-
init(tableView: UITableView, nodeByLocation: [RelayLocation: LocationDataSource.Node]) {
22+
init(
23+
tableView: UITableView,
24+
reuseIdentifier: String
25+
) {
2226
self.tableView = tableView
23-
self.nodeByLocation = nodeByLocation
27+
self.reuseIdentifier = reuseIdentifier
2428
}
2529

26-
func makeCell(for item: RelayLocation, indexPath: IndexPath) -> UITableViewCell {
30+
func makeCell(for item: LocationCellViewModel, indexPath: IndexPath) -> UITableViewCell {
2731
let cell = tableView.dequeueReusableCell(
28-
withIdentifier: LocationDataSource.CellReuseIdentifiers.locationCell.rawValue,
32+
withIdentifier: reuseIdentifier,
2933
for: indexPath
3034
)
3135

@@ -34,17 +38,17 @@ final class LocationCellFactory: CellFactoryProtocol {
3438
return cell
3539
}
3640

37-
func configureCell(_ cell: UITableViewCell, item: RelayLocation, indexPath: IndexPath) {
41+
func configureCell(_ cell: UITableViewCell, item: LocationCellViewModel, indexPath: IndexPath) {
3842
guard let cell = cell as? SelectLocationCell,
39-
let node = nodeByLocation[item] else { return }
43+
let node = delegate?.node(for: item) else { return }
4044

4145
cell.accessibilityIdentifier = node.location.stringRepresentation
4246
cell.isDisabled = !node.isActive
4347
cell.locationLabel.text = node.displayName
4448
cell.showsCollapseControl = node.isCollapsible
4549
cell.isExpanded = node.showsChildren
4650
cell.didCollapseHandler = { [weak self] _ in
47-
self?.delegate?.collapseCell(for: item)
51+
self?.delegate?.toggleCell(for: item)
4852
}
4953
}
5054
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//
2+
// LocationCellViewModel.swift
3+
// MullvadVPN
4+
//
5+
// Created by Mojgan on 2024-02-05.
6+
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import MullvadTypes
10+
11+
struct LocationCellViewModel: Hashable {
12+
let group: SelectLocationSection
13+
let location: RelayLocation
14+
}

0 commit comments

Comments
 (0)