diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index f178e1cdcd7f..442cbea2da58 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -587,6 +587,7 @@ 7AA513862BC91C6B00D081A4 /* LogRotationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA513852BC91C6B00D081A4 /* LogRotationTests.swift */; }; 7AB2B6702BA1EB8C00B03E3B /* ListCustomListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB2B66E2BA1EB8C00B03E3B /* ListCustomListViewController.swift */; }; 7AB2B6712BA1EB8C00B03E3B /* ListCustomListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB2B66F2BA1EB8C00B03E3B /* ListCustomListCoordinator.swift */; }; + 7AB3BEB52BD7A6CB00E34384 /* LocationViewControllerWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB3BEB42BD7A6CB00E34384 /* LocationViewControllerWrapper.swift */; }; 7AB4CCB92B69097E006037F5 /* IPOverrideTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4CCB82B69097E006037F5 /* IPOverrideTests.swift */; }; 7AB4CCBB2B691BBB006037F5 /* IPOverrideInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4CCBA2B691BBB006037F5 /* IPOverrideInteractor.swift */; }; 7ABCA5B32A9349F20044A708 /* Routing.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A88DCCE2A8FABBE00D2FF0E /* Routing.framework */; }; @@ -1848,6 +1849,7 @@ 7AA513852BC91C6B00D081A4 /* LogRotationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogRotationTests.swift; sourceTree = ""; }; 7AB2B66E2BA1EB8C00B03E3B /* ListCustomListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListCustomListViewController.swift; sourceTree = ""; }; 7AB2B66F2BA1EB8C00B03E3B /* ListCustomListCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListCustomListCoordinator.swift; sourceTree = ""; }; + 7AB3BEB42BD7A6CB00E34384 /* LocationViewControllerWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationViewControllerWrapper.swift; sourceTree = ""; }; 7AB4CCB82B69097E006037F5 /* IPOverrideTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideTests.swift; sourceTree = ""; }; 7AB4CCBA2B691BBB006037F5 /* IPOverrideInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideInteractor.swift; sourceTree = ""; }; 7ABE318C2A1CDD4500DF4963 /* UIFont+Weight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Weight.swift"; sourceTree = ""; }; @@ -2461,6 +2463,7 @@ F050AE512B70DFC0003F4EDB /* LocationSection.swift */, F0BE65362B9F136A005CC385 /* LocationSectionHeaderView.swift */, 5888AD86227B17950051EB06 /* LocationViewController.swift */, + 7AB3BEB42BD7A6CB00E34384 /* LocationViewControllerWrapper.swift */, ); path = SelectLocation; sourceTree = ""; @@ -5429,6 +5432,7 @@ 063687BA28EB234F00BE7161 /* PacketTunnelTransport.swift in Sources */, A9C342C12ACC37E30045F00E /* TunnelStatusBlockObserver.swift in Sources */, 587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */, + 7AB3BEB52BD7A6CB00E34384 /* LocationViewControllerWrapper.swift in Sources */, F09D04BD2AEBB7C5003D4F89 /* OutgoingConnectionService.swift in Sources */, 58FF9FF42B07C61B00E4C97D /* AccessMethodValidationError.swift in Sources */, 5896AE84246D5889005B36CB /* CustomDateComponentsFormatting.swift in Sources */, diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift index 174408f0afd8..0514804021ba 100644 --- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift +++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift @@ -81,6 +81,7 @@ public enum AccessibilityIdentifier: String { case outOfTimeView case termsOfServiceView case selectLocationView + case selectLocationViewWrapper case selectLocationTableView case settingsTableView case vpnSettingsTableView diff --git a/ios/MullvadVPN/Coordinators/LocationCoordinator.swift b/ios/MullvadVPN/Coordinators/LocationCoordinator.swift index 30fe23d8e3fa..1a20fcb13c99 100644 --- a/ios/MullvadVPN/Coordinators/LocationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/LocationCoordinator.swift @@ -24,10 +24,10 @@ class LocationCoordinator: Coordinator, Presentable, Presenting { navigationController } - var locationViewController: LocationViewController? { + var locationViewControllerWrapper: LocationViewControllerWrapper? { return navigationController.viewControllers.first { - $0 is LocationViewController - } as? LocationViewController + $0 is LocationViewControllerWrapper + } as? LocationViewControllerWrapper } var relayFilter: RelayFilter { @@ -54,43 +54,14 @@ class LocationCoordinator: Coordinator, Presentable, Presenting { } func start() { - let locationViewController = LocationViewController(customListRepository: customListRepository) - locationViewController.delegate = self - - locationViewController.didSelectRelays = { [weak self] locations in - guard let self else { return } - - var relayConstraints = tunnelManager.settings.relayConstraints - relayConstraints.locations = .only(locations) - - tunnelManager.updateSettings([.relayConstraints(relayConstraints)]) { - self.tunnelManager.startTunnel() - } - - didFinish?(self) - } - - locationViewController.navigateToFilter = { [weak self] in - guard let self else { return } - - let coordinator = makeRelayFilterCoordinator(forModalPresentation: true) - coordinator.start() - - presentChild(coordinator, animated: true) - } - - locationViewController.didUpdateFilter = { [weak self] filter in - guard let self else { return } - - var relayConstraints = tunnelManager.settings.relayConstraints - relayConstraints.filter = .only(filter) - - tunnelManager.updateSettings([.relayConstraints(relayConstraints)]) - } + let locationViewControllerWrapper = LocationViewControllerWrapper( + customListRepository: customListRepository, + selectedRelays: tunnelManager.settings.relayConstraints.locations.value + ) + locationViewControllerWrapper.delegate = self - locationViewController.didFinish = { [weak self] in + locationViewControllerWrapper.didFinish = { [weak self] in guard let self else { return } - didFinish?(self) } @@ -98,12 +69,10 @@ class LocationCoordinator: Coordinator, Presentable, Presenting { if let cachedRelays = try? relayCacheTracker.getCachedRelays() { self.cachedRelays = cachedRelays - locationViewController.setCachedRelays(cachedRelays, filter: relayFilter) + locationViewControllerWrapper.setCachedRelays(cachedRelays, filter: relayFilter) } - locationViewController.relayLocations = tunnelManager.settings.relayConstraints.locations.value - - navigationController.pushViewController(locationViewController, animated: false) + navigationController.pushViewController(locationViewControllerWrapper, animated: false) } private func makeRelayFilterCoordinator(forModalPresentation isModalPresentation: Bool) @@ -118,7 +87,7 @@ class LocationCoordinator: Coordinator, Presentable, Presenting { relayFilterCoordinator.didFinish = { [weak self] coordinator, filter in if let cachedRelays = self?.cachedRelays, let filter { - self?.locationViewController?.setCachedRelays(cachedRelays, filter: filter) + self?.locationViewControllerWrapper?.setCachedRelays(cachedRelays, filter: filter) } coordinator.dismiss(animated: true) @@ -138,7 +107,7 @@ class LocationCoordinator: Coordinator, Presentable, Presenting { coordinator.didFinish = { [weak self] addCustomListCoordinator in addCustomListCoordinator.dismiss(animated: true) - self?.locationViewController?.refreshCustomLists() + self?.locationViewControllerWrapper?.refreshCustomLists() } coordinator.start() @@ -155,7 +124,7 @@ class LocationCoordinator: Coordinator, Presentable, Presenting { coordinator.didFinish = { [weak self] listCustomListCoordinator in listCustomListCoordinator.dismiss(animated: true) - self?.locationViewController?.refreshCustomLists() + self?.locationViewControllerWrapper?.refreshCustomLists() } coordinator.start() @@ -169,7 +138,7 @@ class LocationCoordinator: Coordinator, Presentable, Presenting { // See showEditCustomLists() above. extension LocationCoordinator: UIAdaptivePresentationControllerDelegate { func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { - locationViewController?.refreshCustomLists() + locationViewControllerWrapper?.refreshCustomLists() } } @@ -180,12 +149,37 @@ extension LocationCoordinator: RelayCacheTrackerObserver { ) { self.cachedRelays = cachedRelays - locationViewController?.setCachedRelays(cachedRelays, filter: relayFilter) + locationViewControllerWrapper?.setCachedRelays(cachedRelays, filter: relayFilter) } } -extension LocationCoordinator: LocationViewControllerDelegate { - func didRequestRouteToCustomLists(_ controller: LocationViewController, nodes: [LocationNode]) { +extension LocationCoordinator: LocationViewControllerWrapperDelegate { + func didSelectRelays(relays: UserSelectedRelays) { + var relayConstraints = tunnelManager.settings.relayConstraints + relayConstraints.locations = .only(relays) + + tunnelManager.updateSettings([.relayConstraints(relayConstraints)]) { + self.tunnelManager.startTunnel() + } + + didFinish?(self) + } + + func didUpdateFilter(filter: RelayFilter) { + var relayConstraints = tunnelManager.settings.relayConstraints + relayConstraints.filter = .only(filter) + + tunnelManager.updateSettings([.relayConstraints(relayConstraints)]) + } + + func navigateToFilter() { + let coordinator = makeRelayFilterCoordinator(forModalPresentation: true) + coordinator.start() + + presentChild(coordinator, animated: true) + } + + func navigateToCustomLists(nodes: [LocationNode]) { let actionSheet = UIAlertController( title: NSLocalizedString( "CUSTOM_LIST_ACTION_SHEET_TITLE", diff --git a/ios/MullvadVPN/UI appearance/UIColor+Palette.swift b/ios/MullvadVPN/UI appearance/UIColor+Palette.swift index 5cc9a562a111..bc53819d1030 100644 --- a/ios/MullvadVPN/UI appearance/UIColor+Palette.swift +++ b/ios/MullvadVPN/UI appearance/UIColor+Palette.swift @@ -136,6 +136,11 @@ extension UIColor { static let actionButtonColor = UIColor(white: 1.0, alpha: 0.8) } + enum SegmentedControl { + static let backgroundColor = UIColor(red: 0.18, green: 0.33, blue: 0.49, alpha: 1.0) + static let selectedColor = successColor + } + // Common colors static let primaryColor = UIColor(red: 0.16, green: 0.30, blue: 0.45, alpha: 1.0) static let secondaryColor = UIColor(red: 0.10, green: 0.18, blue: 0.27, alpha: 1.0) diff --git a/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterView.swift b/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterView.swift index ab66f9b3ff00..5a952b146b43 100644 --- a/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterView.swift +++ b/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterView.swift @@ -94,8 +94,8 @@ class RelayFilterView: UIView { contentContainer.spacing = UIMetrics.FilterView.labelSpacing addConstrainedSubviews([contentContainer]) { - contentContainer.pinEdges(.init([.top(0), .bottom(0)]), to: self) - contentContainer.pinEdges(.init([.leading(0), .trailing(0)]), to: layoutMarginsGuide) + contentContainer.pinEdges(.init([.top(4), .bottom(0)]), to: self) + contentContainer.pinEdges(.init([.leading(4), .trailing(4)]), to: layoutMarginsGuide) } } diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift index 11b5c0bbb70c..8e47259ef962 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift @@ -6,14 +6,15 @@ // Copyright © 2019 Mullvad VPN AB. All rights reserved. // -import MullvadLogging import MullvadREST import MullvadSettings import MullvadTypes import UIKit protocol LocationViewControllerDelegate: AnyObject { - func didRequestRouteToCustomLists(_ controller: LocationViewController, nodes: [LocationNode]) + func navigateToCustomLists(nodes: [LocationNode]) + func didSelectRelays(relays: UserSelectedRelays) + func didUpdateFilter(filter: RelayFilter) } final class LocationViewController: UIViewController { @@ -24,7 +25,7 @@ final class LocationViewController: UIViewController { private var dataSource: LocationDataSource? private var cachedRelays: CachedRelays? private var filter = RelayFilter() - var relayLocations: UserSelectedRelays? + private var selectedRelays: UserSelectedRelays? weak var delegate: LocationViewControllerDelegate? var customListRepository: CustomListRepositoryProtocol @@ -36,13 +37,9 @@ final class LocationViewController: UIViewController { return (filter.ownership == .any) && (filter.providers == .any) } - var navigateToFilter: (() -> Void)? - var didSelectRelays: ((UserSelectedRelays) -> Void)? - var didUpdateFilter: ((RelayFilter) -> Void)? - var didFinish: (() -> Void)? - - init(customListRepository: CustomListRepositoryProtocol) { + init(customListRepository: CustomListRepositoryProtocol, selectedRelays: UserSelectedRelays?) { self.customListRepository = customListRepository + self.selectedRelays = selectedRelays super.init(nibName: nil, bundle: nil) } @@ -58,32 +55,6 @@ final class LocationViewController: UIViewController { view.accessibilityIdentifier = .selectLocationView view.backgroundColor = .secondaryColor - navigationItem.title = NSLocalizedString( - "NAVIGATION_TITLE", - tableName: "SelectLocation", - value: "Select location", - comment: "" - ) - - navigationItem.leftBarButtonItem = UIBarButtonItem( - title: NSLocalizedString( - "NAVIGATION_TITLE", - tableName: "SelectLocation", - value: "Filter", - comment: "" - ), - primaryAction: UIAction(handler: { [weak self] _ in - self?.navigateToFilter?() - }) - ) - - navigationItem.rightBarButtonItem = UIBarButtonItem( - systemItem: .done, - primaryAction: UIAction(handler: { [weak self] _ in - self?.didFinish?() - }) - ) - setUpDataSources() setUpTableView() setUpTopContent() @@ -92,7 +63,7 @@ final class LocationViewController: UIViewController { topContentView.pinEdgesToSuperviewMargins(.all().excluding(.bottom)) tableView.pinEdgesToSuperview(.all().excluding(.top)) - tableView.topAnchor.constraint(equalTo: topContentView.bottomAnchor) + tableView.topAnchor.constraint(equalTo: topContentView.bottomAnchor, constant: 8) } } @@ -115,11 +86,11 @@ final class LocationViewController: UIViewController { filterView.setFilter(filter) } - dataSource?.setRelays(cachedRelays.relays, selectedRelays: relayLocations, filter: filter) + dataSource?.setRelays(cachedRelays.relays, selectedRelays: selectedRelays, filter: filter) } func refreshCustomLists() { - dataSource?.refreshCustomLists(selectedRelays: relayLocations) + dataSource?.refreshCustomLists(selectedRelays: selectedRelays) } // MARK: - Private @@ -135,16 +106,15 @@ final class LocationViewController: UIViewController { ) dataSource?.didSelectRelayLocations = { [weak self] locations in - self?.didSelectRelays?(locations) + self?.delegate?.didSelectRelays(relays: locations) } dataSource?.didTapEditCustomLists = { [weak self] in - guard let self else { return } - delegate?.didRequestRouteToCustomLists(self, nodes: allLocationDataSource.nodes) + self?.delegate?.navigateToCustomLists(nodes: allLocationDataSource.nodes) } if let cachedRelays { - dataSource?.setRelays(cachedRelays.relays, selectedRelays: relayLocations, filter: filter) + dataSource?.setRelays(cachedRelays.relays, selectedRelays: selectedRelays, filter: filter) } } @@ -170,7 +140,7 @@ final class LocationViewController: UIViewController { guard let self else { return } filter = $0 - didUpdateFilter?($0) + delegate?.didUpdateFilter(filter: $0) if let cachedRelays { setCachedRelays(cachedRelays, filter: filter) diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift new file mode 100644 index 000000000000..6e310c153596 --- /dev/null +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift @@ -0,0 +1,154 @@ +// +// LocationViewControllerWrapper.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-04-23. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import MullvadREST +import MullvadSettings +import MullvadTypes +import UIKit + +protocol LocationViewControllerWrapperDelegate: AnyObject { + func navigateToCustomLists(nodes: [LocationNode]) + func navigateToFilter() + func didSelectRelays(relays: UserSelectedRelays) + func didUpdateFilter(filter: RelayFilter) +} + +final class LocationViewControllerWrapper: UIViewController { + private let locationViewController: LocationViewController + private let segmentedControl = UISegmentedControl() + + weak var delegate: LocationViewControllerWrapperDelegate? + + init(customListRepository: CustomListRepositoryProtocol, selectedRelays: UserSelectedRelays?) { + locationViewController = LocationViewController( + customListRepository: customListRepository, + selectedRelays: selectedRelays + ) + + super.init(nibName: nil, bundle: nil) + + locationViewController.delegate = self + } + + var didFinish: (() -> Void)? + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.accessibilityIdentifier = .selectLocationViewWrapper + view.backgroundColor = .secondaryColor + + setUpNavigation() + setUpSegmentedControl() + addSubviews() + } + + func setCachedRelays(_ cachedRelays: CachedRelays, filter: RelayFilter) { + locationViewController.setCachedRelays(cachedRelays, filter: filter) + } + + func refreshCustomLists() { + locationViewController.refreshCustomLists() + } + + private func setUpNavigation() { + navigationItem.largeTitleDisplayMode = .never + + navigationItem.title = NSLocalizedString( + "NAVIGATION_TITLE", + tableName: "SelectLocation", + value: "Select location", + comment: "" + ) + + navigationItem.leftBarButtonItem = UIBarButtonItem( + title: NSLocalizedString( + "NAVIGATION_FILTER", + tableName: "SelectLocation", + value: "Filter", + comment: "" + ), + primaryAction: UIAction(handler: { [weak self] _ in + self?.delegate?.navigateToFilter() + }) + ) + + navigationItem.rightBarButtonItem = UIBarButtonItem( + systemItem: .done, + primaryAction: UIAction(handler: { [weak self] _ in + self?.didFinish?() + }) + ) + } + + private func setUpSegmentedControl() { + segmentedControl.backgroundColor = .SegmentedControl.backgroundColor + segmentedControl.selectedSegmentTintColor = .SegmentedControl.selectedColor + segmentedControl.setTitleTextAttributes([ + .foregroundColor: UIColor.white, + .font: UIFont.systemFont(ofSize: 17, weight: .medium), + ], for: .normal) + + segmentedControl.insertSegment(withTitle: NSLocalizedString( + "SEGMENTED_CONTROL_ENTRY", + tableName: "SelectLocation", + value: "Entry", + comment: "" + ), at: 0, animated: false) + segmentedControl.insertSegment(withTitle: NSLocalizedString( + "SEGMENTED_CONTROL_EXIT", + tableName: "SelectLocation", + value: "Exit", + comment: "" + ), at: 1, animated: false) + + segmentedControl.selectedSegmentIndex = 0 + segmentedControl.addTarget(self, action: #selector(segmentedControlDidChange), for: .valueChanged) + } + + private func addSubviews() { + addChild(locationViewController) + locationViewController.didMove(toParent: self) + + view.addConstrainedSubviews([segmentedControl, locationViewController.view]) { + segmentedControl.heightAnchor.constraint(equalToConstant: 44) + segmentedControl.pinEdgesToSuperviewMargins(PinnableEdges([.top(0), .leading(8), .trailing(8)])) + + locationViewController.view.pinEdgesToSuperview(.all().excluding(.top)) + + #if DEBUG + locationViewController.view.topAnchor.constraint(equalTo: segmentedControl.bottomAnchor, constant: 4) + #else + locationViewController.view.pinEdgeToSuperviewMargin(.top(0)) + #endif + } + } + + @objc + private func segmentedControlDidChange(sender: UISegmentedControl) { + refreshCustomLists() + } +} + +extension LocationViewControllerWrapper: LocationViewControllerDelegate { + func navigateToCustomLists(nodes: [LocationNode]) { + delegate?.navigateToCustomLists(nodes: nodes) + } + + func didSelectRelays(relays: UserSelectedRelays) { + delegate?.didSelectRelays(relays: relays) + } + + func didUpdateFilter(filter: RelayFilter) { + delegate?.didUpdateFilter(filter: filter) + } +}