Skip to content

Commit 2ec1843

Browse files
author
Jon Petersson
committed
Combine parts of LocationDataSource and AddLocationDataSource to one
1 parent 563ce04 commit 2ec1843

13 files changed

+277
-379
lines changed

ios/MullvadVPN.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,7 @@
531531
7A6389EB2B7FAD7A008E77E1 /* SettingsFieldValidationErrorContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389EA2B7FAD7A008E77E1 /* SettingsFieldValidationErrorContentView.swift */; };
532532
7A6389ED2B7FADA1008E77E1 /* SettingsFieldValidationErrorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389EC2B7FADA1008E77E1 /* SettingsFieldValidationErrorConfiguration.swift */; };
533533
7A6389F82B864CDF008E77E1 /* LocationNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389F72B864CDF008E77E1 /* LocationNode.swift */; };
534+
7A6652B82BB44C3E0042D848 /* LocationDiffableDataSourceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6652B62BB44B120042D848 /* LocationDiffableDataSourceProtocol.swift */; };
534535
7A6B4F592AB8412E00123853 /* TunnelMonitorTimings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */; };
535536
7A6F2FA52AFA3CB2006D0856 /* AccountExpiryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */; };
536537
7A6F2FA72AFBB9AE006D0856 /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */; };
@@ -1787,6 +1788,7 @@
17871788
7A6389EA2B7FAD7A008E77E1 /* SettingsFieldValidationErrorContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFieldValidationErrorContentView.swift; sourceTree = "<group>"; };
17881789
7A6389EC2B7FADA1008E77E1 /* SettingsFieldValidationErrorConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFieldValidationErrorConfiguration.swift; sourceTree = "<group>"; };
17891790
7A6389F72B864CDF008E77E1 /* LocationNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationNode.swift; sourceTree = "<group>"; };
1791+
7A6652B62BB44B120042D848 /* LocationDiffableDataSourceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationDiffableDataSourceProtocol.swift; sourceTree = "<group>"; };
17901792
7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorTimings.swift; sourceTree = "<group>"; };
17911793
7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryTests.swift; sourceTree = "<group>"; };
17921794
7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiry.swift; sourceTree = "<group>"; };
@@ -2432,6 +2434,7 @@
24322434
F050AE4D2B70D7F8003F4EDB /* LocationCellViewModel.swift */,
24332435
583DA21325FA4B5C00318683 /* LocationDataSource.swift */,
24342436
F050AE5D2B739A73003F4EDB /* LocationDataSourceProtocol.swift */,
2437+
7A6652B62BB44B120042D848 /* LocationDiffableDataSourceProtocol.swift */,
24352438
7A6389F72B864CDF008E77E1 /* LocationNode.swift */,
24362439
F050AE512B70DFC0003F4EDB /* LocationSection.swift */,
24372440
F0BE65362B9F136A005CC385 /* LocationSectionHeaderView.swift */,
@@ -5321,6 +5324,7 @@
53215324
58607A4D2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift in Sources */,
53225325
58C8191829FAA2C400DEB1B4 /* NotificationConfiguration.swift in Sources */,
53235326
58FF9FE82B07650A00E4C97D /* ButtonCellContentConfiguration.swift in Sources */,
5327+
7A6652B82BB44C3E0042D848 /* LocationDiffableDataSourceProtocol.swift in Sources */,
53245328
5827B0A82B0F49EF00CCBBA1 /* ProxyConfigurationInteractorProtocol.swift in Sources */,
53255329
7A5869B92B56E7F000640D27 /* IPOverrideViewControllerDelegate.swift in Sources */,
53265330
586C0D7A2B039CE300E7CDD7 /* ShadowsocksCipherPicker.swift in Sources */,

ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift

+1-9
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,6 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
4343
*/
4444
private let secondaryNavigationContainer = RootContainerViewController()
4545

46-
private var customListRepository: CustomListRepositoryProtocol {
47-
#if DEBUG
48-
InMemoryCustomListRepository()
49-
#else
50-
CustomListRepository()
51-
#endif
52-
}
53-
5446
/// Posts `preferredAccountNumber` notification when user inputs the account number instead of voucher code
5547
private let preferredAccountNumberSubject = PassthroughSubject<String, Never>()
5648

@@ -719,7 +711,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
719711
navigationController: navigationController,
720712
tunnelManager: tunnelManager,
721713
relayCacheTracker: relayCacheTracker,
722-
customListRepository: customListRepository
714+
customListRepository: CustomListRepository()
723715
)
724716

725717
locationCoordinator.didFinish = { [weak self] _ in

ios/MullvadVPN/Coordinators/CustomLists/AddLocationsDataSource.swift

+57-181
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,37 @@
66
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
77
//
88

9-
import Foundation
109
import MullvadSettings
1110
import MullvadTypes
1211
import UIKit
1312

14-
class AddLocationsDataSource: UITableViewDiffableDataSource<LocationSection, LocationCellViewModel> {
15-
private let tableView: UITableView
13+
class AddLocationsDataSource:
14+
UITableViewDiffableDataSource<LocationSection, LocationCellViewModel>,
15+
LocationDiffableDataSourceProtocol
16+
{
1617
private let nodes: [LocationNode]
17-
private var customListLocationNode: CustomListLocationNode
18-
var didUpdateCustomList: ((CustomListLocationNode) -> Void)?
18+
private var selectedLocations: [RelayLocation]
19+
var didUpdateLocations: (([RelayLocation]) -> Void)?
20+
let tableView: UITableView
21+
let sections: [LocationSection]
1922

2023
init(
2124
tableView: UITableView,
22-
allLocations: [LocationNode],
23-
customList: CustomList
25+
allLocationNodes: [LocationNode],
26+
selectedLocations: [RelayLocation]
2427
) {
2528
self.tableView = tableView
26-
self.nodes = allLocations
29+
self.nodes = allLocationNodes
30+
self.selectedLocations = selectedLocations
2731

28-
self.customListLocationNode = CustomListLocationNodeBuilder(
29-
customList: customList,
30-
allLocations: self.nodes
31-
).customListLocationNode
32+
let sections: [LocationSection] = [.customLists]
33+
self.sections = sections
3234

3335
super.init(tableView: tableView) { _, indexPath, itemIdentifier in
3436
let cell = tableView.dequeueReusableView(
35-
withIdentifier: LocationSection.allCases[indexPath.section],
37+
withIdentifier: sections[indexPath.section],
3638
for: indexPath
37-
// swiftlint:disable:next force_cast
38-
) as! LocationCell
39+
) as! LocationCell // swiftlint:disable:this force_cast
3940
cell.configure(item: itemIdentifier, behavior: .add)
4041
cell.selectionStyle = .none
4142
return cell
@@ -48,86 +49,35 @@ class AddLocationsDataSource: UITableViewDiffableDataSource<LocationSection, Loc
4849
}
4950

5051
private func reloadWithSelectedLocations() {
51-
var locationsList: [LocationCellViewModel] = []
52-
nodes.forEach { node in
53-
let viewModel = LocationCellViewModel(
54-
section: .customLists,
55-
node: node,
56-
isSelected: customListLocationNode.children.contains(node)
57-
)
58-
locationsList.append(viewModel)
59-
60-
// Determine if the node should be expanded.
61-
guard isLocationInCustomList(node: node) else {
62-
return
63-
}
64-
65-
// Walk tree backwards to determine which nodes should be expanded.
66-
node.forEachAncestor { node in
67-
node.showsChildren = true
52+
var items = nodes.flatMap { node in
53+
// Create a "faux" root node to use for constructing a node tree.
54+
let rootNode = RootLocationNode(children: [node])
55+
56+
// Only parents with partially selected children should be expanded.
57+
node.forEachDescendant { descendantNode in
58+
if selectedLocations.containsAny(locations: descendantNode.locations) {
59+
descendantNode.parent?.showsChildren = true
60+
}
6861
}
6962

70-
locationsList.append(contentsOf: recursivelyCreateCellViewModelTree(
71-
for: node,
63+
// Construct node tree.
64+
return recursivelyCreateCellViewModelTree(
65+
for: rootNode,
7266
in: .customLists,
73-
indentationLevel: 1
74-
))
75-
}
76-
updateDataSnapshot(with: locationsList)
77-
}
78-
79-
private func updateDataSnapshot(
80-
with list: [LocationCellViewModel],
81-
animated: Bool = false,
82-
completion: (() -> Void)? = nil
83-
) {
84-
var snapshot = NSDiffableDataSourceSnapshot<LocationSection, LocationCellViewModel>()
85-
86-
snapshot.appendSections([.customLists])
87-
snapshot.appendItems(list, toSection: .customLists)
88-
89-
apply(snapshot, animatingDifferences: animated, completion: completion)
90-
}
91-
92-
private func recursivelyCreateCellViewModelTree(
93-
for node: LocationNode,
94-
in section: LocationSection,
95-
indentationLevel: Int
96-
) -> [LocationCellViewModel] {
97-
var viewModels = [LocationCellViewModel]()
98-
for childNode in node.children {
99-
viewModels.append(
100-
LocationCellViewModel(
101-
section: .customLists,
102-
node: childNode,
103-
indentationLevel: indentationLevel,
104-
isSelected: customListLocationNode.children.contains(childNode)
105-
)
67+
indentationLevel: 0
10668
)
69+
}
10770

108-
let indentationLevel = indentationLevel + 1
109-
110-
// Walk tree forward to determine which nodes should be expanded.
111-
if isLocationInCustomList(node: childNode) {
112-
viewModels.append(
113-
contentsOf: recursivelyCreateCellViewModelTree(
114-
for: childNode,
115-
in: section,
116-
indentationLevel: indentationLevel
117-
)
118-
)
71+
// Apply selection to node tree.
72+
items = items.map { item in
73+
var item = item
74+
if selectedLocations.containsAny(locations: item.node.locations) {
75+
item.isSelected = true
11976
}
77+
return item
12078
}
12179

122-
return viewModels
123-
}
124-
125-
private func isLocationInCustomList(node: LocationNode) -> Bool {
126-
customListLocationNode.children.contains(where: { containsChild(parent: node, child: $0) })
127-
}
128-
129-
private func containsChild(parent: LocationNode, child: LocationNode) -> Bool {
130-
parent.flattened.contains(child)
80+
updateDataSnapshot(with: [items], reloadExisting: false)
13181
}
13282

13383
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
@@ -146,68 +96,40 @@ extension AddLocationsDataSource: UITableViewDelegate {
14696

14797
extension AddLocationsDataSource: LocationCellDelegate {
14898
func toggleExpanding(cell: LocationCell) {
149-
guard let indexPath = tableView.indexPath(for: cell),
150-
let item = itemIdentifier(for: indexPath) else { return }
151-
let isExpanded = item.node.showsChildren
152-
153-
item.node.showsChildren = !isExpanded
154-
155-
var locationList = snapshot().itemIdentifiers
156-
157-
if !isExpanded {
158-
locationList.addSubNodes(from: item, at: indexPath)
159-
} else {
160-
locationList.removeSubNodes(from: item.node)
161-
}
162-
163-
updateDataSnapshot(with: locationList, animated: true, completion: {
164-
self.scroll(to: item, animated: true)
165-
})
99+
toggle(cell: cell)
166100
}
167101

168-
func toggleSelection(cell: LocationCell) {
102+
func toggleSelecting(cell: LocationCell) {
169103
guard let index = tableView.indexPath(for: cell)?.row else { return }
170104

171-
var locationList = snapshot().itemIdentifiers
172-
let item = locationList[index]
105+
var items = snapshot().itemIdentifiers
106+
let item = items[index]
107+
108+
guard let nodeLocation = item.node.locations.first else { return }
109+
173110
let isSelected = !item.isSelected
174-
locationList[index].isSelected = isSelected
111+
items[index].isSelected = isSelected
175112

176-
locationList.deselectAncestors(from: item.node)
177-
locationList.toggleSelectionSubNodes(from: item.node, isSelected: isSelected)
113+
items.deselectAncestors(from: item.node)
114+
items.toggleSelectionSubNodes(from: item.node, isSelected: isSelected)
178115

179116
if isSelected {
180-
customListLocationNode.add(selectedLocation: item.node)
117+
selectedLocations.append(nodeLocation)
181118
} else {
182-
customListLocationNode.remove(selectedLocation: item.node, with: locationList)
119+
selectedLocations.removeAll { $0 == nodeLocation }
183120
}
184-
updateDataSnapshot(with: locationList, completion: {
185-
self.didUpdateCustomList?(self.customListLocationNode)
121+
122+
updateDataSnapshot(with: [items], reloadExisting: true, completion: {
123+
self.didUpdateLocations?(self.selectedLocations)
186124
})
187125
}
188126
}
189127

190-
extension AddLocationsDataSource {
191-
private func scroll(to item: LocationCellViewModel, animated: Bool) {
192-
guard
193-
let visibleIndexPaths = tableView.indexPathsForVisibleRows,
194-
let indexPath = indexPath(for: item)
195-
else { return }
196-
197-
if item.node.children.count > visibleIndexPaths.count {
198-
tableView.scrollToRow(at: indexPath, at: .top, animated: animated)
199-
} else {
200-
if let last = item.node.children.last {
201-
if let lastInsertedIndexPath = self.indexPath(for: LocationCellViewModel(
202-
section: .customLists,
203-
node: last
204-
)),
205-
let lastVisibleIndexPath = visibleIndexPaths.last,
206-
lastInsertedIndexPath >= lastVisibleIndexPath {
207-
tableView.scrollToRow(at: lastInsertedIndexPath, at: .bottom, animated: animated)
208-
}
209-
}
210-
}
128+
fileprivate extension [RelayLocation] {
129+
func containsAny(locations: [RelayLocation]) -> Bool {
130+
locations.contains(where: { location in
131+
contains(location)
132+
})
211133
}
212134
}
213135

@@ -232,49 +154,3 @@ fileprivate extension [LocationCellViewModel] {
232154
}
233155
}
234156
}
235-
236-
// MARK: - Update custom list
237-
238-
fileprivate extension CustomListLocationNode {
239-
func remove(selectedLocation: LocationNode, with locationList: [LocationCellViewModel]) {
240-
if let index = children.firstIndex(of: selectedLocation) {
241-
children.remove(at: index)
242-
}
243-
removeAncestors(node: selectedLocation)
244-
addSiblings(from: locationList, for: selectedLocation)
245-
}
246-
247-
func add(selectedLocation: LocationNode) {
248-
children.append(selectedLocation)
249-
removeSubNodes(node: selectedLocation)
250-
}
251-
252-
private func removeSubNodes(node: LocationNode) {
253-
node.forEachDescendant { child in
254-
// removing children if they are already added to custom list
255-
if let index = children.firstIndex(of: child) {
256-
children.remove(at: index)
257-
}
258-
}
259-
}
260-
261-
private func removeAncestors(node: LocationNode) {
262-
node.forEachAncestor { parent in
263-
if let index = children.firstIndex(of: parent) {
264-
children.remove(at: index)
265-
}
266-
}
267-
}
268-
269-
private func addSiblings(from locationList: [LocationCellViewModel], for node: LocationNode) {
270-
guard let parent = node.parent else { return }
271-
parent.children.forEach { child in
272-
// adding siblings if they are already selected in snapshot
273-
if let item = locationList.first(where: { $0.node == child }),
274-
item.isSelected && !children.contains(child) {
275-
children.append(child)
276-
}
277-
}
278-
addSiblings(from: locationList, for: parent)
279-
}
280-
}

0 commit comments

Comments
 (0)