Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Intercept back button when leaving an unsaved custom list #6079

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,7 @@
7A6F2FAB2AFD3097006D0856 /* CustomDNSCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FAA2AFD3097006D0856 /* CustomDNSCellFactory.swift */; };
7A6F2FAD2AFD3DA7006D0856 /* CustomDNSViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FAC2AFD3DA7006D0856 /* CustomDNSViewController.swift */; };
7A6F2FAF2AFE36E7006D0856 /* VPNSettingsInfoButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FAE2AFE36E7006D0856 /* VPNSettingsInfoButtonItem.swift */; };
7A7907332BC0280A00B61F81 /* InterceptibleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7907322BC0280A00B61F81 /* InterceptibleNavigationController.swift */; };
7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */; };
7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */; };
7A83A0C62B29A750008B5CE7 /* APIAccessMethodsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A83A0C52B29A750008B5CE7 /* APIAccessMethodsTests.swift */; };
Expand Down Expand Up @@ -1805,6 +1806,7 @@
7A6F2FAA2AFD3097006D0856 /* CustomDNSCellFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDNSCellFactory.swift; sourceTree = "<group>"; };
7A6F2FAC2AFD3DA7006D0856 /* CustomDNSViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDNSViewController.swift; sourceTree = "<group>"; };
7A6F2FAE2AFE36E7006D0856 /* VPNSettingsInfoButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSettingsInfoButtonItem.swift; sourceTree = "<group>"; };
7A7907322BC0280A00B61F81 /* InterceptibleNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterceptibleNavigationController.swift; sourceTree = "<group>"; };
7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstTimeLaunch.swift; sourceTree = "<group>"; };
7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootConfiguration.swift; sourceTree = "<group>"; };
7A83A0C52B29A750008B5CE7 /* APIAccessMethodsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIAccessMethodsTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2694,6 +2696,7 @@
58138E60294871C600684F0C /* DeviceDataThrottling.swift */,
7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */,
582AE30F2440A6CA00E6733A /* InputTextFormatter.swift */,
7A7907322BC0280A00B61F81 /* InterceptibleNavigationController.swift */,
58DFF7D12B0256A300F864E0 /* MarkdownStylingOptions.swift */,
58CC40EE24A601900019D96E /* ObserverList.swift */,
);
Expand Down Expand Up @@ -5463,6 +5466,7 @@
7A42DEC92A05164100B209BE /* SettingsInputCell.swift in Sources */,
5803B4B22940A48700C23744 /* TunnelStore.swift in Sources */,
586A950F29012BEE007BAF2B /* AddressCacheTracker.swift in Sources */,
7A7907332BC0280A00B61F81 /* InterceptibleNavigationController.swift in Sources */,
F02F41A02B9723AF00625A4F /* AddLocationsViewController.swift in Sources */,
587B753D2666468F00DEF7E9 /* NotificationController.swift in Sources */,
);
Expand Down
38 changes: 38 additions & 0 deletions ios/MullvadVPN/Classes/InterceptibleNavigationController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// InterceptibleNavigationController.swift
// MullvadVPN
//
// Created by Jon Petersson on 2024-04-05.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import UIKit

class InterceptibleNavigationController: CustomNavigationController {
var shouldPopViewController: ((UIViewController) -> Bool)?
var shouldPopToViewController: ((UIViewController) -> Bool)?

// Called when popping the topmost view controller in the stack, eg. by pressing a navigation
// bar back button.
@discardableResult
override func popViewController(animated: Bool) -> UIViewController? {
guard let viewController = viewControllers.last else { return nil }

if shouldPopViewController?(viewController) ?? true {
return super.popViewController(animated: animated)
} else {
return nil
}
}

// Called when popping to a specific view controller, eg. by long pressing a navigation bar
// back button (revealing a navigation menu) and selecting a destination view controller.
@discardableResult
override func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? {
if shouldPopToViewController?(viewController) ?? true {
return super.popToViewController(viewController, animated: animated)
} else {
return nil
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,10 @@ extension AddCustomListCoordinator: CustomListViewControllerDelegate {
let coordinator = AddLocationsCoordinator(
navigationController: navigationController,
nodes: nodes,
customList: list
subject: subject
)

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
))
coordinator.didFinish = { locationsCoordinator in
locationsCoordinator.removeFromParent()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Combine
import MullvadSettings
import MullvadTypes
import Routing
Expand All @@ -14,9 +15,9 @@ import UIKit
class AddLocationsCoordinator: Coordinator, Presentable, Presenting {
private let navigationController: UINavigationController
private let nodes: [LocationNode]
private var customList: CustomList
private var subject: CurrentValueSubject<CustomListViewModel, Never>

var didFinish: ((AddLocationsCoordinator, CustomList) -> Void)?
var didFinish: ((AddLocationsCoordinator) -> Void)?

var presentedViewController: UIViewController {
navigationController
Expand All @@ -25,17 +26,17 @@ class AddLocationsCoordinator: Coordinator, Presentable, Presenting {
init(
navigationController: UINavigationController,
nodes: [LocationNode],
customList: CustomList
subject: CurrentValueSubject<CustomListViewModel, Never>
) {
self.navigationController = navigationController
self.nodes = nodes
self.customList = customList
self.subject = subject
}

func start() {
let controller = AddLocationsViewController(
allLocationsNodes: nodes,
customList: customList
subject: subject
)
controller.delegate = self

Expand All @@ -51,11 +52,7 @@ class AddLocationsCoordinator: Coordinator, Presentable, Presenting {
}

extension AddLocationsCoordinator: AddLocationsViewControllerDelegate {
func didUpdateSelectedLocations(locations: [RelayLocation]) {
customList.locations = locations
}

func didBack() {
didFinish?(self, customList)
didFinish?(self)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Combine
import MullvadSettings
import MullvadTypes
import UIKit
Expand All @@ -15,20 +16,21 @@ class AddLocationsDataSource:
LocationDiffableDataSourceProtocol {
private var customListLocationNode: CustomListLocationNode
private let nodes: [LocationNode]
var didUpdateCustomList: ((CustomListLocationNode) -> Void)?
private let subject: CurrentValueSubject<CustomListViewModel, Never>
let tableView: UITableView
let sections: [LocationSection]

init(
tableView: UITableView,
allLocationNodes: [LocationNode],
customList: CustomList
subject: CurrentValueSubject<CustomListViewModel, Never>
) {
self.tableView = tableView
self.nodes = allLocationNodes
self.subject = subject

self.customListLocationNode = CustomListLocationNodeBuilder(
customList: customList,
customList: subject.value.customList,
allLocations: self.nodes
).customListLocationNode

Expand All @@ -51,10 +53,12 @@ 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)
}
Expand Down Expand Up @@ -149,7 +153,10 @@ extension AddLocationsDataSource: LocationCellDelegate {
customListLocationNode.remove(selectedLocation: item.node, with: locationList)
}
updateDataSnapshot(with: [locationList], completion: {
self.didUpdateCustomList?(self.customListLocationNode)
let locations = self.customListLocationNode.children.reduce([]) { partialResult, locationNode in
partialResult + locationNode.locations
}
self.subject.value.locations = locations
})
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Combine
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
private let subject: CurrentValueSubject<CustomListViewModel, Never>

weak var delegate: AddLocationsViewControllerDelegate?
private let tableView: UITableView = {
Expand All @@ -33,10 +33,10 @@ class AddLocationsViewController: UIViewController {

init(
allLocationsNodes: [LocationNode],
customList: CustomList
subject: CurrentValueSubject<CustomListViewModel, Never>
) {
self.nodes = allLocationsNodes
self.customList = customList
self.subject = subject
super.init(nibName: nil, bundle: nil)
}

Expand Down Expand Up @@ -70,17 +70,8 @@ class AddLocationsViewController: UIViewController {
dataSource = AddLocationsDataSource(
tableView: tableView,
allLocationNodes: nodes.copy(),
customList: customList
subject: subject
)

dataSource?.didUpdateCustomList = { [weak self] customListLocationNode in
guard let self else { return }
delegate?.didUpdateSelectedLocations(
locations: customListLocationNode.children.reduce([]) { partialResult, locationNode in
partialResult + locationNode.locations
}
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ class CustomListViewController: UIViewController {
private let alertPresenter: AlertPresenter
private var validationErrors: Set<CustomListFieldValidationError> = []

private var persistedCustomList: CustomList? {
return interactor.fetchAll().first(where: { $0.id == subject.value.id })
}

var hasUnsavedChanges: Bool {
persistedCustomList != subject.value.customList
}

private lazy var cellConfiguration: CustomListCellConfiguration = {
CustomListCellConfiguration(tableView: tableView, subject: subject)
}()
Expand Down Expand Up @@ -75,12 +83,12 @@ class CustomListViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()

navigationItem.rightBarButtonItem = saveBarButton
view.directionalLayoutMargins = UIMetrics.contentLayoutMargins
view.backgroundColor = .secondaryColor
isModalInPresentation = true

addSubviews()
configureNavigationItem()
configureDataSource()
configureTableView()

Expand All @@ -90,10 +98,6 @@ class CustomListViewController: UIViewController {
}.store(in: &cancellables)
}

private func configureNavigationItem() {
navigationItem.rightBarButtonItem = saveBarButton
}

private func configureTableView() {
tableView.delegate = dataSourceConfiguration
tableView.backgroundColor = .secondaryColor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@ struct CustomListViewModel {
var customList: CustomList {
CustomList(id: id, name: name, locations: locations)
}

mutating func update(with list: CustomList) {
name = list.name
locations = list.locations
}
}
Loading
Loading