summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2024-06-25 11:02:22 +0200
committerBug Magnet <marco.nikic@mullvad.net>2024-06-25 11:02:22 +0200
commit7f0a3cb04acdefcbc4fe2ded766042651d373292 (patch)
treeda44ff36f4fa3456c1b855ecfbc42d69afdc228f
parent6f5acb52ebac1d5968738ee97127c27dc3373ea0 (diff)
parent9ce987252bf989e380a7b4e8c13aaedc9a40433d (diff)
downloadmullvadvpn-7f0a3cb04acdefcbc4fe2ded766042651d373292.tar.xz
mullvadvpn-7f0a3cb04acdefcbc4fe2ded766042651d373292.zip
Merge branch 'update-view-model-when-switching-between-entry-and-exit-ios-631'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/AddLocationsDataSource.swift26
-rw-r--r--ios/MullvadVPN/Coordinators/CustomLists/ListCustomListCoordinator.swift43
-rw-r--r--ios/MullvadVPN/Coordinators/LocationCoordinator.swift19
-rw-r--r--ios/MullvadVPN/UI appearance/UIColor+Palette.swift2
-rw-r--r--ios/MullvadVPN/View controllers/RelayFilter/RelayFilterView.swift2
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/LocationCell.swift14
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/LocationCellViewModel.swift14
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift111
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/LocationDiffableDataSourceProtocol.swift11
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift18
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift171
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/RelaySelection.swift22
-rw-r--r--ios/MullvadVPNTests/MullvadLogging/LogRotationTests.swift8
14 files changed, 343 insertions, 122 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 10e3e3ab8a..87040fb11a 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -488,6 +488,7 @@
7A3353912AAA014400F0A71C /* SimulatorVPNConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353902AAA014400F0A71C /* SimulatorVPNConnection.swift */; };
7A3353932AAA089000F0A71C /* SimulatorTunnelInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */; };
7A3353972AAA0F8600F0A71C /* OperationBlockObserverSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */; };
+ 7A3EFAAB2BDFDAE800318736 /* RelaySelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3EFAAA2BDFDAE800318736 /* RelaySelection.swift */; };
7A3FD1B52AD4465A0042BEA6 /* AppMessageHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */; };
7A3FD1B72AD54ABD0042BEA6 /* AnyTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB982A98F4ED00F578F2 /* AnyTransport.swift */; };
7A3FD1B82AD54AE60042BEA6 /* TimeServerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB9A2A98F58600F578F2 /* TimeServerProxy.swift */; };
@@ -1873,6 +1874,7 @@
7A3353902AAA014400F0A71C /* SimulatorVPNConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorVPNConnection.swift; sourceTree = "<group>"; };
7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelInfo.swift; sourceTree = "<group>"; };
7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationBlockObserverSupport.swift; sourceTree = "<group>"; };
+ 7A3EFAAA2BDFDAE800318736 /* RelaySelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelection.swift; sourceTree = "<group>"; };
7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMessageHandlerTests.swift; sourceTree = "<group>"; };
7A42DEC82A05164100B209BE /* SettingsInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInputCell.swift; sourceTree = "<group>"; };
7A45CFC22C05FF2F00D80B21 /* ScreenshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotTests.swift; sourceTree = "<group>"; };
@@ -2820,6 +2822,7 @@
F0BE65362B9F136A005CC385 /* LocationSectionHeaderView.swift */,
5888AD86227B17950051EB06 /* LocationViewController.swift */,
7AB3BEB42BD7A6CB00E34384 /* LocationViewControllerWrapper.swift */,
+ 7A3EFAAA2BDFDAE800318736 /* RelaySelection.swift */,
);
path = SelectLocation;
sourceTree = "<group>";
@@ -5878,6 +5881,7 @@
5878F50029CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift in Sources */,
7A28826D2BAAC9DE00FD9F20 /* IPOverrideHeaderView.swift in Sources */,
A98502032B627B120061901E /* LocalNetworkProbe.swift in Sources */,
+ 7A3EFAAB2BDFDAE800318736 /* RelaySelection.swift in Sources */,
7A6F2FA92AFD0842006D0856 /* CustomDNSDataSource.swift in Sources */,
58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */,
5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */,
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsDataSource.swift b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsDataSource.swift
index 0af9f14d48..a180b206aa 100644
--- a/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsDataSource.swift
+++ b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsDataSource.swift
@@ -53,16 +53,6 @@ class AddLocationsDataSource:
reloadWithSelectedLocations()
}
- // Called from `LocationDiffableDataSourceProtocol`.
- func nodeShowsChildren(_ node: LocationNode) -> Bool {
- isLocationInCustomList(node: node)
- }
-
- // Called from `LocationDiffableDataSourceProtocol`.
- func nodeShouldBeSelected(_ node: LocationNode) -> Bool {
- customListLocationNode.children.contains(node)
- }
-
private func reloadWithSelectedLocations() {
var locationsList: [LocationCellViewModel] = []
nodes.forEach { node in
@@ -161,6 +151,22 @@ extension AddLocationsDataSource: LocationCellDelegate {
}
}
+// MARK: - Called from LocationDiffableDataSourceProtocol
+
+extension AddLocationsDataSource {
+ func nodeShowsChildren(_ node: LocationNode) -> Bool {
+ isLocationInCustomList(node: node)
+ }
+
+ func nodeShouldBeSelected(_ node: LocationNode) -> Bool {
+ customListLocationNode.children.contains(node)
+ }
+
+ func excludedRelayTitle(_ node: LocationNode) -> String? {
+ nil // N/A
+ }
+}
+
// MARK: - Toggle selection in table view
fileprivate extension [LocationCellViewModel] {
diff --git a/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListCoordinator.swift b/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListCoordinator.swift
index ff69c62887..1bd748f75e 100644
--- a/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/CustomLists/ListCustomListCoordinator.swift
@@ -60,9 +60,25 @@ class ListCustomListCoordinator: Coordinator, Presentable, Presenting {
coordinator.didFinish = { [weak self] editCustomListCoordinator, action, list in
guard let self else { return }
+
popToList()
editCustomListCoordinator.removeFromParent()
- self.updateRelayConstraints(for: action, in: list)
+
+ var relayConstraints = tunnelManager.settings.relayConstraints
+ relayConstraints.entryLocations = self.updateRelayConstraint(
+ relayConstraints.entryLocations,
+ for: action,
+ in: list
+ )
+ relayConstraints.exitLocations = self.updateRelayConstraint(
+ relayConstraints.exitLocations,
+ for: action,
+ in: list
+ )
+
+ tunnelManager.updateSettings([.relayConstraints(relayConstraints)]) { [weak self] in
+ self?.tunnelManager.reconnectTunnel(selectNewRelay: true)
+ }
}
coordinator.didCancel = { [weak self] editCustomListCoordinator in
@@ -75,38 +91,39 @@ class ListCustomListCoordinator: Coordinator, Presentable, Presenting {
addChild(coordinator)
}
- private func updateRelayConstraints(for action: EditCustomListCoordinator.FinishAction, in list: CustomList) {
- var relayConstraints = tunnelManager.settings.relayConstraints
+ private func updateRelayConstraint(
+ _ relayConstraint: RelayConstraint<UserSelectedRelays>,
+ for action: EditCustomListCoordinator.FinishAction,
+ in list: CustomList
+ ) -> RelayConstraint<UserSelectedRelays> {
+ var relayConstraint = relayConstraint
- guard let customListSelection = relayConstraints.exitLocations.value?.customListSelection,
+ guard let customListSelection = relayConstraint.value?.customListSelection,
customListSelection.listId == list.id
- else { return }
+ else { return relayConstraint }
switch action {
case .save:
- // TODO: - Add entry locations
if customListSelection.isList {
let selectedRelays = UserSelectedRelays(
locations: list.locations,
customListSelection: UserSelectedRelays.CustomListSelection(listId: list.id, isList: true)
)
- relayConstraints.exitLocations = .only(selectedRelays)
+ relayConstraint = .only(selectedRelays)
} else {
let selectedConstraintIsRemovedFromList = list.locations.filter {
- relayConstraints.exitLocations.value?.locations.contains($0) ?? false
+ relayConstraint.value?.locations.contains($0) ?? false
}.isEmpty
if selectedConstraintIsRemovedFromList {
- relayConstraints.exitLocations = .only(UserSelectedRelays(locations: []))
+ relayConstraint = .only(UserSelectedRelays(locations: []))
}
}
case .delete:
- relayConstraints.exitLocations = .only(UserSelectedRelays(locations: []))
+ relayConstraint = .only(UserSelectedRelays(locations: []))
}
- tunnelManager.updateSettings([.relayConstraints(relayConstraints)]) { [weak self] in
- self?.tunnelManager.reconnectTunnel(selectNewRelay: true)
- }
+ return relayConstraint
}
private func popToList() {
diff --git a/ios/MullvadVPN/Coordinators/LocationCoordinator.swift b/ios/MullvadVPN/Coordinators/LocationCoordinator.swift
index 38b9ced8cd..08224e6e75 100644
--- a/ios/MullvadVPN/Coordinators/LocationCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/LocationCoordinator.swift
@@ -54,10 +54,10 @@ class LocationCoordinator: Coordinator, Presentable, Presenting {
}
func start() {
- // TODO: - the location should be defined whether it's Entry or Exit location
let locationViewControllerWrapper = LocationViewControllerWrapper(
customListRepository: customListRepository,
- selectedRelays: tunnelManager.settings.relayConstraints.exitLocations.value
+ constraints: tunnelManager.settings.relayConstraints,
+ multihopEnabled: tunnelManager.settings.tunnelMultihopState == .on
)
locationViewControllerWrapper.delegate = self
@@ -155,18 +155,25 @@ extension LocationCoordinator: RelayCacheTrackerObserver {
}
extension LocationCoordinator: LocationViewControllerWrapperDelegate {
- func didSelectRelays(relays: UserSelectedRelays) {
+ func didSelectEntryRelays(_ relays: UserSelectedRelays) {
var relayConstraints = tunnelManager.settings.relayConstraints
- relayConstraints.exitLocations = .only(relays)
+ relayConstraints.entryLocations = .only(relays)
tunnelManager.updateSettings([.relayConstraints(relayConstraints)]) {
self.tunnelManager.startTunnel()
}
+ }
+
+ func didSelectExitRelays(_ relays: UserSelectedRelays) {
+ var relayConstraints = tunnelManager.settings.relayConstraints
+ relayConstraints.exitLocations = .only(relays)
- didFinish?(self)
+ tunnelManager.updateSettings([.relayConstraints(relayConstraints)]) {
+ self.tunnelManager.startTunnel()
+ }
}
- func didUpdateFilter(filter: RelayFilter) {
+ func didUpdateFilter(_ filter: RelayFilter) {
var relayConstraints = tunnelManager.settings.relayConstraints
relayConstraints.filter = .only(filter)
diff --git a/ios/MullvadVPN/UI appearance/UIColor+Palette.swift b/ios/MullvadVPN/UI appearance/UIColor+Palette.swift
index 1cbd20d21e..38749d6706 100644
--- a/ios/MullvadVPN/UI appearance/UIColor+Palette.swift
+++ b/ios/MullvadVPN/UI appearance/UIColor+Palette.swift
@@ -137,7 +137,7 @@ extension UIColor {
}
enum SegmentedControl {
- static let backgroundColor = UIColor(red: 0.18, green: 0.33, blue: 0.49, alpha: 1.0)
+ static let backgroundColor = UIColor(red: 0.14, green: 0.25, blue: 0.38, alpha: 1.0)
static let selectedColor = successColor
}
diff --git a/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterView.swift b/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterView.swift
index 5a952b146b..60a7520322 100644
--- a/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterView.swift
+++ b/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterView.swift
@@ -94,7 +94,7 @@ class RelayFilterView: UIView {
contentContainer.spacing = UIMetrics.FilterView.labelSpacing
addConstrainedSubviews([contentContainer]) {
- contentContainer.pinEdges(.init([.top(4), .bottom(0)]), to: self)
+ contentContainer.pinEdges(.init([.top(7), .bottom(0)]), to: self)
contentContainer.pinEdges(.init([.leading(4), .trailing(4)]), to: layoutMarginsGuide)
}
}
diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationCell.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationCell.swift
index 4f2f5ab62c..1bb37ba097 100644
--- a/ios/MullvadVPN/View controllers/SelectLocation/LocationCell.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationCell.swift
@@ -72,7 +72,7 @@ class LocationCell: UITableViewCell {
var isDisabled = false {
didSet {
- updateDisabled()
+ updateDisabled(isDisabled)
updateBackgroundColor()
updateStatusIndicatorColor()
}
@@ -150,7 +150,7 @@ class LocationCell: UITableViewCell {
updateCollapseImage()
updateAccessibilityCustomActions()
- updateDisabled()
+ updateDisabled(isDisabled)
updateBackgroundColor()
setLayoutMargins()
@@ -207,7 +207,7 @@ class LocationCell: UITableViewCell {
statusIndicator.backgroundColor = statusIndicatorColor()
}
- private func updateDisabled() {
+ private func updateDisabled(_ isDisabled: Bool) {
locationLabel.alpha = isDisabled ? 0.2 : 1
collapseButton.alpha = isDisabled ? 0.2 : 1
@@ -339,9 +339,17 @@ extension LocationCell {
}
}
+ setExcludedRelayTitle(item.excludedRelayTitle)
setBehavior(behavior)
}
+ private func setExcludedRelayTitle(_ title: String?) {
+ if let title {
+ locationLabel.text! += " (\(title))"
+ updateDisabled(true)
+ }
+ }
+
private func setBehavior(_ newBehavior: LocationCellBehavior) {
self.behavior = newBehavior
updateLeadingImage()
diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationCellViewModel.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationCellViewModel.swift
index 14b7745efd..ca24c60552 100644
--- a/ios/MullvadVPN/View controllers/SelectLocation/LocationCellViewModel.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationCellViewModel.swift
@@ -13,26 +13,29 @@ struct LocationCellViewModel: Hashable {
let node: LocationNode
var indentationLevel = 0
var isSelected = false
+ var excludedRelayTitle: String?
func hash(into hasher: inout Hasher) {
hasher.combine(node)
hasher.combine(node.children.count)
hasher.combine(section)
hasher.combine(isSelected)
- hasher.combine(indentationLevel)
}
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.node == rhs.node &&
lhs.node.children.count == rhs.node.children.count &&
lhs.section == rhs.section &&
- lhs.isSelected == rhs.isSelected &&
- lhs.indentationLevel == rhs.indentationLevel
+ lhs.isSelected == rhs.isSelected
}
}
extension [LocationCellViewModel] {
- mutating func addSubNodes(from item: LocationCellViewModel, at indexPath: IndexPath) {
+ mutating func addSubNodes(
+ from item: LocationCellViewModel,
+ at indexPath: IndexPath,
+ excludedRelayTitleCallback: ((LocationNode) -> String?)?
+ ) {
let section = LocationSection.allCases[indexPath.section]
let row = indexPath.row + 1
@@ -41,7 +44,8 @@ extension [LocationCellViewModel] {
section: section,
node: $0,
indentationLevel: item.indentationLevel + 1,
- isSelected: item.isSelected
+ isSelected: false,
+ excludedRelayTitle: excludedRelayTitleCallback?($0)
)
}
diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift
index 3aa69604e4..3dd3f42795 100644
--- a/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift
@@ -17,7 +17,11 @@ final class LocationDataSource:
LocationDiffableDataSourceProtocol {
private var currentSearchString = ""
private var dataSources: [LocationDataSourceProtocol] = []
- private var selectedItem: LocationCellViewModel?
+ // The selected location.
+ private var selectedLocation: LocationCellViewModel?
+ // When multihop is enabled, this is the "inverted" selected location, ie. entry
+ // if in exit mode and exit if in entry mode.
+ private var excludedLocation: LocationCellViewModel?
let tableView: UITableView
let sections: [LocationSection]
@@ -50,7 +54,7 @@ final class LocationDataSource:
defaultRowAnimation = .fade
}
- func setRelays(_ response: REST.ServerRelaysResponse, selectedRelays: UserSelectedRelays?, filter: RelayFilter) {
+ func setRelays(_ response: REST.ServerRelaysResponse, selectedRelays: RelaySelection, filter: RelayFilter) {
let allLocationsDataSource =
dataSources.first(where: { $0 is AllLocationDataSource }) as? AllLocationDataSource
@@ -64,7 +68,7 @@ final class LocationDataSource:
allLocationsDataSource?.reload(response, relays: relays)
customListsDataSource?.reload(allLocationNodes: allLocationsDataSource?.nodes ?? [])
- mapSelectedItem(from: selectedRelays)
+ setSelectedRelays(selectedRelays)
filterRelays(by: currentSearchString)
}
@@ -81,8 +85,10 @@ final class LocationDataSource:
}
updateDataSnapshot(with: list, reloadExisting: !searchString.isEmpty) {
+ self.tableView.reloadData()
+
if searchString.isEmpty {
- self.setSelectedItem(self.selectedItem, animated: false, completion: {
+ self.updateSelection(self.selectedLocation, animated: false, completion: {
self.scrollToSelectedRelay()
})
} else {
@@ -92,7 +98,7 @@ final class LocationDataSource:
}
/// Refreshes the custom list section and keeps all modifications intact (selection and expanded states).
- func refreshCustomLists(selectedRelays: UserSelectedRelays?) {
+ func refreshCustomLists(selectedRelays: RelaySelection) {
guard let allLocationsDataSource =
dataSources.first(where: { $0 is AllLocationDataSource }) as? AllLocationDataSource,
let customListsDataSource =
@@ -110,7 +116,7 @@ final class LocationDataSource:
customListsDataSource.reload(allLocationNodes: allLocationsDataSource.nodes)
// Reapply current selection.
- mapSelectedItem(from: selectedRelays)
+ setSelectedRelays(selectedRelays)
// Reapply current search filter.
let searchResultNodes = dataSources[0].search(by: currentSearchString)
@@ -133,27 +139,28 @@ final class LocationDataSource:
], reloadExisting: true)
}
- func scrollToSelectedRelay() {
- indexPathForSelectedRelay().flatMap {
- tableView.scrollToRow(at: $0, at: .middle, animated: false)
+ func setSelectedRelays(_ selectedRelays: RelaySelection) {
+ selectedLocation = mapSelection(from: selectedRelays.selected)
+
+ if selectedRelays.hasExcludedRelay {
+ excludedLocation = mapSelection(from: selectedRelays.excluded)
+ excludedLocation?.excludedRelayTitle = selectedRelays.excludedTitle
}
- }
- // Called from `LocationDiffableDataSourceProtocol`.
- func nodeShowsChildren(_ node: LocationNode) -> Bool {
- node.showsChildren
+ tableView.reloadData()
}
- // Called from `LocationDiffableDataSourceProtocol`.
- func nodeShouldBeSelected(_ node: LocationNode) -> Bool {
- false
+ func scrollToSelectedRelay() {
+ indexPathForSelectedRelay().flatMap {
+ tableView.scrollToRow(at: $0, at: .middle, animated: false)
+ }
}
private func indexPathForSelectedRelay() -> IndexPath? {
- selectedItem.flatMap { indexPath(for: $0) }
+ selectedLocation.flatMap { indexPath(for: $0) }
}
- private func mapSelectedItem(from selectedRelays: UserSelectedRelays?) {
+ private func mapSelection(from selectedRelays: UserSelectedRelays?) -> LocationCellViewModel? {
let allLocationsDataSource =
dataSources.first(where: { $0 is AllLocationDataSource }) as? AllLocationDataSource
@@ -165,7 +172,7 @@ final class LocationDataSource:
if let customListSelection = selectedRelays.customListSelection,
let customList = customListsDataSource?.customList(by: customListSelection.listId),
let selectedNode = customListsDataSource?.node(by: selectedRelays, for: customList) {
- selectedItem = LocationCellViewModel(
+ return LocationCellViewModel(
section: .customLists,
node: selectedNode,
indentationLevel: selectedNode.hierarchyLevel
@@ -173,47 +180,49 @@ final class LocationDataSource:
// Look for a matching all locations node.
} else if let location = selectedRelays.locations.first,
let selectedNode = allLocationsDataSource?.node(by: location) {
- selectedItem = LocationCellViewModel(
+ return LocationCellViewModel(
section: .allLocations,
node: selectedNode,
indentationLevel: selectedNode.hierarchyLevel
)
}
}
+
+ return nil
}
- private func setSelectedItem(_ item: LocationCellViewModel?, animated: Bool, completion: (() -> Void)? = nil) {
- selectedItem = item
- guard let selectedItem else { return }
+ private func updateSelection(_ item: LocationCellViewModel?, animated: Bool, completion: (() -> Void)? = nil) {
+ selectedLocation = item
+ guard let selectedLocation else { return }
- let rootNode = selectedItem.node.root
+ let rootNode = selectedLocation.node.root
// Exit early if no changes to the node tree should be made.
- guard selectedItem.node != rootNode else {
+ guard selectedLocation.node != rootNode else {
completion?()
return
}
// Make sure we have an index path for the selected item.
guard let indexPath = indexPath(for: LocationCellViewModel(
- section: selectedItem.section,
+ section: selectedLocation.section,
node: rootNode
)) else { return }
// Walk tree backwards to determine which nodes should be expanded.
- selectedItem.node.forEachAncestor { node in
+ selectedLocation.node.forEachAncestor { node in
node.showsChildren = true
}
// Construct node tree.
let nodesToAdd = recursivelyCreateCellViewModelTree(
for: rootNode,
- in: selectedItem.section,
+ in: selectedLocation.section,
indentationLevel: 1
)
// Insert the new node tree below the selected item.
- var snapshotItems = snapshot().itemIdentifiers(inSection: selectedItem.section)
+ var snapshotItems = snapshot().itemIdentifiers(inSection: selectedLocation.section)
snapshotItems.insert(contentsOf: nodesToAdd, at: indexPath.row + 1)
let list = sections.enumerated().map { index, section in
@@ -230,6 +239,12 @@ final class LocationDataSource:
)
}
+ private func nodeMatchesExcludedLocation(_ node: LocationNode) -> Bool {
+ // Compare nodes on name rather than whole node in order to match all items in both .customLists
+ // and .allLocations.
+ node.name == excludedLocation?.node.name
+ }
+
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// swiftlint:disable:next force_cast
let cell = super.tableView(tableView, cellForRowAt: indexPath) as! LocationCell
@@ -238,6 +253,26 @@ final class LocationDataSource:
}
}
+// MARK: - Called from LocationDiffableDataSourceProtocol
+
+extension LocationDataSource {
+ func nodeShowsChildren(_ node: LocationNode) -> Bool {
+ node.showsChildren
+ }
+
+ func nodeShouldBeSelected(_ node: LocationNode) -> Bool {
+ false // N/A
+ }
+
+ func excludedRelayTitle(_ node: LocationNode) -> String? {
+ if nodeMatchesExcludedLocation(node) {
+ excludedLocation?.excludedRelayTitle
+ } else {
+ nil
+ }
+ }
+}
+
extension LocationDataSource: UITableViewDelegate {
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
switch sections[section] {
@@ -271,7 +306,8 @@ extension LocationDataSource: UITableViewDelegate {
}
func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
- itemIdentifier(for: indexPath)?.node.isActive ?? false
+ guard let item = itemIdentifier(for: indexPath) else { return false }
+ return !nodeMatchesExcludedLocation(item.node) && item.node.isActive
}
func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int {
@@ -279,15 +315,16 @@ extension LocationDataSource: UITableViewDelegate {
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
- if let item = itemIdentifier(for: indexPath), item == selectedItem {
- tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
+ if let item = itemIdentifier(for: indexPath), item == selectedLocation {
+ cell.setSelected(true, animated: false)
}
}
func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
if let indexPath = indexPathForSelectedRelay() {
- tableView.deselectRow(at: indexPath, animated: false)
- selectedItem = nil
+ if let cell = tableView.cellForRow(at: indexPath) {
+ cell.setSelected(false, animated: false)
+ }
}
return indexPath
@@ -295,7 +332,7 @@ extension LocationDataSource: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let item = itemIdentifier(for: indexPath) else { return }
- selectedItem = item
+ selectedLocation = item
var customListSelection: UserSelectedRelays.CustomListSelection?
if let topmostNode = item.node.root as? CustomListLocationNode {
@@ -323,7 +360,9 @@ extension LocationDataSource: LocationCellDelegate {
guard let indexPath = tableView.indexPath(for: cell),
let item = itemIdentifier(for: indexPath) else { return }
- let items = toggledItems(for: cell)
+ let items = toggledItems(for: cell, excludedRelayTitleCallback: { node in
+ self.excludedRelayTitle(node)
+ })
updateDataSnapshot(with: items, reloadExisting: true, completion: {
self.scroll(to: item, animated: true)
diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationDiffableDataSourceProtocol.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationDiffableDataSourceProtocol.swift
index 0450be0a81..59f7e12d21 100644
--- a/ios/MullvadVPN/View controllers/SelectLocation/LocationDiffableDataSourceProtocol.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationDiffableDataSourceProtocol.swift
@@ -14,6 +14,7 @@ protocol LocationDiffableDataSourceProtocol: UITableViewDiffableDataSource<Locat
var sections: [LocationSection] { get }
func nodeShowsChildren(_ node: LocationNode) -> Bool
func nodeShouldBeSelected(_ node: LocationNode) -> Bool
+ func excludedRelayTitle(_ node: LocationNode) -> String?
}
extension LocationDiffableDataSourceProtocol {
@@ -39,7 +40,10 @@ extension LocationDiffableDataSourceProtocol {
}
}
- func toggledItems(for cell: LocationCell) -> [[LocationCellViewModel]] {
+ func toggledItems(
+ for cell: LocationCell,
+ excludedRelayTitleCallback: ((LocationNode) -> String?)? = nil
+ ) -> [[LocationCellViewModel]] {
guard let indexPath = tableView.indexPath(for: cell),
let item = itemIdentifier(for: indexPath) else { return [[]] }
@@ -50,7 +54,7 @@ extension LocationDiffableDataSourceProtocol {
item.node.showsChildren = !isExpanded
if !isExpanded {
- locationList.addSubNodes(from: item, at: indexPath)
+ locationList.addSubNodes(from: item, at: indexPath, excludedRelayTitleCallback: excludedRelayTitleCallback)
} else {
locationList.removeSubNodes(from: item.node)
}
@@ -99,7 +103,8 @@ extension LocationDiffableDataSourceProtocol {
section: section,
node: childNode,
indentationLevel: indentationLevel,
- isSelected: nodeShouldBeSelected(childNode)
+ isSelected: nodeShouldBeSelected(childNode),
+ excludedRelayTitle: excludedRelayTitle(childNode)
)
)
diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift
index 38188122ff..4cdfc71446 100644
--- a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift
@@ -25,7 +25,7 @@ final class LocationViewController: UIViewController {
private var dataSource: LocationDataSource?
private var cachedRelays: CachedRelays?
private var filter = RelayFilter()
- private var selectedRelays: UserSelectedRelays?
+ private var selectedRelays: RelaySelection
weak var delegate: LocationViewControllerDelegate?
var customListRepository: CustomListRepositoryProtocol
@@ -37,9 +37,10 @@ final class LocationViewController: UIViewController {
return (filter.ownership == .any) && (filter.providers == .any)
}
- init(customListRepository: CustomListRepositoryProtocol, selectedRelays: UserSelectedRelays?) {
+ init(customListRepository: CustomListRepositoryProtocol, selectedRelays: RelaySelection) {
self.customListRepository = customListRepository
self.selectedRelays = selectedRelays
+
super.init(nibName: nil, bundle: nil)
}
@@ -55,7 +56,7 @@ final class LocationViewController: UIViewController {
view.accessibilityIdentifier = .selectLocationView
view.backgroundColor = .secondaryColor
- setUpDataSources()
+ setUpDataSource()
setUpTableView()
setUpTopContent()
@@ -97,17 +98,22 @@ final class LocationViewController: UIViewController {
dataSource?.refreshCustomLists(selectedRelays: selectedRelays)
}
+ func setSelectedRelays(_ selectedRelays: RelaySelection) {
+ self.selectedRelays = selectedRelays
+ dataSource?.setSelectedRelays(selectedRelays)
+ }
+
// MARK: - Private
- private func setUpDataSources() {
+ private func setUpDataSource() {
dataSource = LocationDataSource(
tableView: tableView,
allLocations: AllLocationDataSource(),
customLists: CustomListsDataSource(repository: customListRepository)
)
- dataSource?.didSelectRelayLocations = { [weak self] locations in
- self?.delegate?.didSelectRelays(relays: locations)
+ dataSource?.didSelectRelayLocations = { [weak self] relays in
+ self?.delegate?.didSelectRelays(relays: relays)
}
dataSource?.didTapEditCustomLists = { [weak self] in
diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift
index 333d965f87..c86ea173c5 100644
--- a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift
+++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift
@@ -14,25 +14,72 @@ import UIKit
protocol LocationViewControllerWrapperDelegate: AnyObject {
func navigateToCustomLists(nodes: [LocationNode])
func navigateToFilter()
- func didSelectRelays(relays: UserSelectedRelays)
- func didUpdateFilter(filter: RelayFilter)
+ func didSelectEntryRelays(_ relays: UserSelectedRelays)
+ func didSelectExitRelays(_ relays: UserSelectedRelays)
+ func didUpdateFilter(_ filter: RelayFilter)
}
final class LocationViewControllerWrapper: UIViewController {
- private let locationViewController: LocationViewController
+ enum MultihopContext: Int, CaseIterable, CustomStringConvertible {
+ case entry, exit
+
+ var description: String {
+ switch self {
+ case .entry:
+ NSLocalizedString(
+ "MULTIHOP_ENTRY",
+ tableName: "SelectLocation",
+ value: "Entry",
+ comment: ""
+ )
+ case .exit:
+ NSLocalizedString(
+ "MULTIHOP_EXIT",
+ tableName: "SelectLocation",
+ value: "Exit",
+ comment: ""
+ )
+ }
+ }
+ }
+
+ private let entryLocationViewController: LocationViewController
+ private let exitLocationViewController: LocationViewController
private let segmentedControl = UISegmentedControl()
+ private let locationViewContainer = UIStackView()
+
+ private var selectedEntry: UserSelectedRelays?
+ private var selectedExit: UserSelectedRelays?
+ private let multihopEnabled: Bool
+ private var multihopContext: MultihopContext = .exit
weak var delegate: LocationViewControllerWrapperDelegate?
- init(customListRepository: CustomListRepositoryProtocol, selectedRelays: UserSelectedRelays?) {
- locationViewController = LocationViewController(
+ init(
+ customListRepository: CustomListRepositoryProtocol,
+ constraints: RelayConstraints,
+ multihopEnabled: Bool
+ ) {
+ self.multihopEnabled = multihopEnabled
+
+ selectedEntry = constraints.entryLocations.value
+ selectedExit = constraints.exitLocations.value
+
+ entryLocationViewController = LocationViewController(
+ customListRepository: customListRepository,
+ selectedRelays: RelaySelection()
+ )
+
+ exitLocationViewController = LocationViewController(
customListRepository: customListRepository,
- selectedRelays: selectedRelays
+ selectedRelays: RelaySelection()
)
super.init(nibName: nil, bundle: nil)
- locationViewController.delegate = self
+ updateViewControllers {
+ $0.delegate = self
+ }
}
var didFinish: (() -> Void)?
@@ -50,14 +97,25 @@ final class LocationViewControllerWrapper: UIViewController {
setUpNavigation()
setUpSegmentedControl()
addSubviews()
+ swapViewController()
}
func setCachedRelays(_ cachedRelays: CachedRelays, filter: RelayFilter) {
- locationViewController.setCachedRelays(cachedRelays, filter: filter)
+ updateViewControllers {
+ $0.setCachedRelays(cachedRelays, filter: filter)
+ }
}
func refreshCustomLists() {
- locationViewController.refreshCustomLists()
+ updateViewControllers {
+ $0.refreshCustomLists()
+ }
+ }
+
+ private func updateViewControllers(callback: (LocationViewController) -> Void) {
+ [entryLocationViewController, exitLocationViewController]
+ .compactMap { $0 }
+ .forEach { callback($0) }
}
private func setUpNavigation() {
@@ -100,44 +158,76 @@ final class LocationViewControllerWrapper: UIViewController {
.font: UIFont.systemFont(ofSize: 17, weight: .medium),
], for: .normal)
- segmentedControl.insertSegment(withTitle: NSLocalizedString(
- "MULTIHOP_TAB_ENTRY",
- tableName: "SelectLocation",
- value: "Entry",
- comment: ""
- ), at: 0, animated: false)
- segmentedControl.insertSegment(withTitle: NSLocalizedString(
- "MULTIHOP_TAB_EXIT",
- tableName: "SelectLocation",
- value: "Exit",
- comment: ""
- ), at: 1, animated: false)
+ segmentedControl.insertSegment(
+ withTitle: MultihopContext.entry.description,
+ at: MultihopContext.entry.rawValue,
+ animated: false
+ )
+ segmentedControl.insertSegment(
+ withTitle: MultihopContext.exit.description,
+ at: MultihopContext.exit.rawValue,
+ animated: false
+ )
- segmentedControl.selectedSegmentIndex = 0
+ segmentedControl.selectedSegmentIndex = multihopContext.rawValue
segmentedControl.addTarget(self, action: #selector(segmentedControlDidChange), for: .valueChanged)
}
private func addSubviews() {
- addChild(locationViewController)
- locationViewController.didMove(toParent: self)
-
- view.addConstrainedSubviews([segmentedControl, locationViewController.view]) {
+ view.addConstrainedSubviews([segmentedControl, locationViewContainer]) {
segmentedControl.heightAnchor.constraint(equalToConstant: 44)
segmentedControl.pinEdgesToSuperviewMargins(PinnableEdges([.top(0), .leading(8), .trailing(8)]))
- locationViewController.view.pinEdgesToSuperview(.all().excluding(.top))
+ locationViewContainer.pinEdgesToSuperview(.all().excluding(.top))
- #if DEBUG
- locationViewController.view.topAnchor.constraint(equalTo: segmentedControl.bottomAnchor, constant: 4)
- #else
- locationViewController.view.pinEdgeToSuperviewMargin(.top(0))
- #endif
+ if multihopEnabled {
+ locationViewContainer.topAnchor.constraint(equalTo: segmentedControl.bottomAnchor, constant: 4)
+ } else {
+ locationViewContainer.pinEdgeToSuperviewMargin(.top(0))
+ }
}
}
@objc
private func segmentedControlDidChange(sender: UISegmentedControl) {
- refreshCustomLists()
+ multihopContext = .allCases[segmentedControl.selectedSegmentIndex]
+ swapViewController()
+ }
+
+ func swapViewController() {
+ locationViewContainer.arrangedSubviews.forEach { view in
+ view.removeFromSuperview()
+ }
+
+ let (selectedRelays, oldViewController, newViewController) = switch multihopContext {
+ case .entry:
+ (
+ RelaySelection(
+ selected: selectedEntry,
+ excluded: selectedExit,
+ excludedTitle: MultihopContext.exit.description
+ ),
+ exitLocationViewController,
+ entryLocationViewController
+ )
+ case .exit:
+ (
+ RelaySelection(
+ selected: selectedExit,
+ excluded: multihopEnabled ? selectedEntry : nil,
+ excludedTitle: MultihopContext.entry.description
+ ),
+ entryLocationViewController,
+ exitLocationViewController
+ )
+ }
+
+ oldViewController.removeFromParent()
+ newViewController.setSelectedRelays(selectedRelays)
+ addChild(newViewController)
+ newViewController.didMove(toParent: self)
+
+ locationViewContainer.addArrangedSubview(newViewController.view)
}
}
@@ -147,10 +237,21 @@ extension LocationViewControllerWrapper: LocationViewControllerDelegate {
}
func didSelectRelays(relays: UserSelectedRelays) {
- delegate?.didSelectRelays(relays: relays)
+ switch multihopContext {
+ case .entry:
+ selectedEntry = relays
+ delegate?.didSelectEntryRelays(relays)
+
+ // Trigger change in segmented control, which in turn triggers view controller swap.
+ segmentedControl.selectedSegmentIndex = MultihopContext.exit.rawValue
+ segmentedControl.sendActions(for: .valueChanged)
+ case .exit:
+ delegate?.didSelectExitRelays(relays)
+ didFinish?()
+ }
}
func didUpdateFilter(filter: RelayFilter) {
- delegate?.didUpdateFilter(filter: filter)
+ delegate?.didUpdateFilter(filter)
}
}
diff --git a/ios/MullvadVPN/View controllers/SelectLocation/RelaySelection.swift b/ios/MullvadVPN/View controllers/SelectLocation/RelaySelection.swift
new file mode 100644
index 0000000000..5ae7354fe8
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/SelectLocation/RelaySelection.swift
@@ -0,0 +1,22 @@
+//
+// RelaySelection.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-04-29.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadTypes
+
+struct RelaySelection {
+ var selected: UserSelectedRelays?
+ var excluded: UserSelectedRelays?
+ var excludedTitle: String?
+
+ var hasExcludedRelay: Bool {
+ if excluded?.locations.count == 1, case .hostname = excluded?.locations.first {
+ return true
+ }
+ return false
+ }
+}
diff --git a/ios/MullvadVPNTests/MullvadLogging/LogRotationTests.swift b/ios/MullvadVPNTests/MullvadLogging/LogRotationTests.swift
index d67b3099dc..9ea45fe8e9 100644
--- a/ios/MullvadVPNTests/MullvadLogging/LogRotationTests.swift
+++ b/ios/MullvadVPNTests/MullvadLogging/LogRotationTests.swift
@@ -11,11 +11,13 @@ import XCTest
final class LogRotationTests: XCTestCase {
let fileManager = FileManager.default
- let directoryPath = FileManager.default.temporaryDirectory
- .appendingPathComponent("LogRotationTests", isDirectory: true)
+ var directoryPath: URL!
override func setUpWithError() throws {
- try? fileManager.createDirectory(
+ directoryPath = FileManager.default.temporaryDirectory
+ .appendingPathComponent("LogRotationTests", isDirectory: true)
+
+ try fileManager.createDirectory(
at: directoryPath,
withIntermediateDirectories: true
)