Skip to content

Commit

Permalink
Move tracking unsaved changes into coordinator
Browse files Browse the repository at this point in the history
  • Loading branch information
mojganii authored and Jon Petersson committed Apr 12, 2024
1 parent 7081464 commit 660b9df
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 107 deletions.
8 changes: 4 additions & 4 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +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 */; };
7A7907332BC0280A00B61F81 /* InterceptableNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7907322BC0280A00B61F81 /* InterceptableNavigationController.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 @@ -1806,7 +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>"; };
7A7907322BC0280A00B61F81 /* InterceptableNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterceptableNavigationController.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 @@ -2696,7 +2696,7 @@
58138E60294871C600684F0C /* DeviceDataThrottling.swift */,
7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */,
582AE30F2440A6CA00E6733A /* InputTextFormatter.swift */,
7A7907322BC0280A00B61F81 /* InterceptibleNavigationController.swift */,
7A7907322BC0280A00B61F81 /* InterceptableNavigationController.swift */,
58DFF7D12B0256A300F864E0 /* MarkdownStylingOptions.swift */,
58CC40EE24A601900019D96E /* ObserverList.swift */,
);
Expand Down Expand Up @@ -5466,7 +5466,7 @@
7A42DEC92A05164100B209BE /* SettingsInputCell.swift in Sources */,
5803B4B22940A48700C23744 /* TunnelStore.swift in Sources */,
586A950F29012BEE007BAF2B /* AddressCacheTracker.swift in Sources */,
7A7907332BC0280A00B61F81 /* InterceptibleNavigationController.swift in Sources */,
7A7907332BC0280A00B61F81 /* InterceptableNavigationController.swift in Sources */,
F02F41A02B9723AF00625A4F /* AddLocationsViewController.swift in Sources */,
587B753D2666468F00DEF7E9 /* NotificationController.swift in Sources */,
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// InterceptibleNavigationController.swift
// InterceptableNavigationController.swift
// MullvadVPN
//
// Created by Jon Petersson on 2024-04-05.
Expand All @@ -8,12 +8,13 @@

import UIKit

class InterceptibleNavigationController: CustomNavigationController {
class InterceptableNavigationController: 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 }

Expand All @@ -26,6 +27,7 @@ class InterceptibleNavigationController: CustomNavigationController {

// 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class CustomListViewController: UIViewController {
return interactor.fetchAll().first(where: { $0.id == subject.value.id })
}

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

Expand Down Expand Up @@ -83,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 @@ -98,39 +98,6 @@ class CustomListViewController: UIViewController {
}.store(in: &cancellables)
}

private func configureNavigationItem() {
if let navigationController = navigationController as? InterceptibleNavigationController {
interceptNavigation(navigationController)
}

navigationController?.interactivePopGestureRecognizer?.delegate = self
navigationItem.rightBarButtonItem = saveBarButton
}

private func interceptNavigation(_ navigationController: InterceptibleNavigationController) {
navigationController.shouldPopViewController = { [weak self] viewController in
guard
let self,
viewController is Self,
hasUnsavedChanges
else { return true }

self.onUnsavedChanges()
return false
}

navigationController.shouldPopToViewController = { [weak self] viewController in
guard
let self,
viewController is ListCustomListViewController,
hasUnsavedChanges
else { return true }

self.onUnsavedChanges()
return false
}
}

private func configureTableView() {
tableView.delegate = dataSourceConfiguration
tableView.backgroundColor = .secondaryColor
Expand Down Expand Up @@ -232,68 +199,4 @@ class CustomListViewController: UIViewController {

alertPresenter.showAlert(presentation: presentation, animated: true)
}

@objc private func onUnsavedChanges() {
let message = NSMutableAttributedString(
markdownString: NSLocalizedString(
"CUSTOM_LISTS_UNSAVED_CHANGES_PROMPT",
tableName: "CustomLists",
value: "You have unsaved changes.",
comment: ""
),
options: MarkdownStylingOptions(font: .preferredFont(forTextStyle: .body))
)

let presentation = AlertPresentation(
id: "api-custom-lists-unsaved-changes-alert",
icon: .alert,
attributedMessage: message,
buttons: [
AlertAction(
title: NSLocalizedString(
"CUSTOM_LISTS_DISCARD_CHANGES_BUTTON",
tableName: "CustomLists",
value: "Discard changes",
comment: ""
),
style: .destructive,
handler: {
// Reset subject/view model to no longer having unsaved changes.
if let persistedCustomList = self.persistedCustomList {
self.subject.value.update(with: persistedCustomList)
}
self.delegate?.customListDidSave(self.subject.value.customList)
}
),
AlertAction(
title: NSLocalizedString(
"CUSTOM_LISTS_BACK_TO_EDITING_BUTTON",
tableName: "CustomLists",
value: "Back to editing",
comment: ""
),
style: .default
),
]
)

alertPresenter.showAlert(presentation: presentation, animated: true)
}
}

extension CustomListViewController: UIGestureRecognizerDelegate {
// For some reason, intercepting `popViewController(animated: Bool)` in `InterceptibleNavigationController`
// by SWIPING back leads to weird behaviour where subsequent navigation seem to happen systemwise but not
// UI-wise. This leads to the UI freezing up, and the only remedy is to restart the app.
//
// To get around this issue we can intercept the back swipe gesture and manually perform the transition
// instead, thereby bypassing the inner mechanisms that seem to go out of sync.
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == navigationController?.interactivePopGestureRecognizer else {
return true
}

navigationController?.popViewController(animated: true)
return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@ class EditCustomListCoordinator: Coordinator, Presentable, Presenting {
let customList: CustomList
let nodes: [LocationNode]
let subject: CurrentValueSubject<CustomListViewModel, Never>
private lazy var alertPresenter: AlertPresenter = {
AlertPresenter(context: self)
}()

var presentedViewController: UIViewController {
navigationController
}

var didFinish: ((EditCustomListCoordinator, FinishAction, CustomList) -> Void)?
var didCancel: ((EditCustomListCoordinator) -> Void)?

init(
navigationController: UINavigationController,
Expand All @@ -50,7 +54,7 @@ class EditCustomListCoordinator: Coordinator, Presentable, Presenting {
let controller = CustomListViewController(
interactor: customListInteractor,
subject: subject,
alertPresenter: AlertPresenter(context: self)
alertPresenter: alertPresenter
)
controller.delegate = self

Expand All @@ -61,7 +65,76 @@ class EditCustomListCoordinator: Coordinator, Presentable, Presenting {
comment: ""
)

navigationController.interactivePopGestureRecognizer?.delegate = self
navigationController.pushViewController(controller, animated: true)

guard let interceptableNavigationController = navigationController as? InterceptableNavigationController else {
return
}

interceptableNavigationController.shouldPopViewController = { [weak self] viewController in
guard
let self,
let customListViewController = viewController as? CustomListViewController,
customListViewController.hasUnsavedChanges
else { return true }
presentUnsavedChangesDialog()
return false
}

interceptableNavigationController.shouldPopToViewController = { [weak self] viewController in
guard
let self,
let customListViewController = viewController as? CustomListViewController,
customListViewController.hasUnsavedChanges
else { return true }

presentUnsavedChangesDialog()
return false
}
}

private func presentUnsavedChangesDialog() {
let message = NSMutableAttributedString(
markdownString: NSLocalizedString(
"CUSTOM_LISTS_UNSAVED_CHANGES_PROMPT",
tableName: "CustomLists",
value: "You have unsaved changes.",
comment: ""
),
options: MarkdownStylingOptions(font: .preferredFont(forTextStyle: .body))
)

let presentation = AlertPresentation(
id: "api-custom-lists-unsaved-changes-alert",
icon: .alert,
attributedMessage: message,
buttons: [
AlertAction(
title: NSLocalizedString(
"CUSTOM_LISTS_DISCARD_CHANGES_BUTTON",
tableName: "CustomLists",
value: "Discard changes",
comment: ""
),
style: .destructive,
handler: {
self.didCancel?(self)
}
),
AlertAction(
title: NSLocalizedString(
"CUSTOM_LISTS_BACK_TO_EDITING_BUTTON",
tableName: "CustomLists",
value: "Back to editing",
comment: ""
),
style: .default
),
]
)

alertPresenter.showAlert(presentation: presentation, animated: true)
}
}

Expand Down Expand Up @@ -90,3 +163,19 @@ extension EditCustomListCoordinator: CustomListViewControllerDelegate {
addChild(coordinator)
}
}

extension EditCustomListCoordinator: UIGestureRecognizerDelegate {
// For some reason, intercepting `popViewController(animated: Bool)` in `InterceptibleNavigationController`
// by SWIPING back leads to weird behaviour where subsequent navigation seem to happen systemwise but not
// UI-wise. This leads to the UI freezing up, and the only remedy is to restart the app.
//
// To get around this issue we can intercept the back swipe gesture and manually perform the transition
// instead, thereby bypassing the inner mechanisms that seem to go out of sync.
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == navigationController.interactivePopGestureRecognizer else {
return true
}
navigationController.popViewController(animated: true)
return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ class ListCustomListCoordinator: Coordinator, Presentable, Presenting {
self.updateRelayConstraints(for: action, in: list)
}

coordinator.didCancel = { [weak self] editCustomListCoordinator in
guard let self else { return }
popToList()
editCustomListCoordinator.removeFromParent()
}

coordinator.start()
addChild(coordinator)
}
Expand Down
2 changes: 1 addition & 1 deletion ios/MullvadVPN/Coordinators/LocationCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ class LocationCoordinator: Coordinator, Presentable, Presenting {

private func showEditCustomLists(nodes: [LocationNode]) {
let coordinator = ListCustomListCoordinator(
navigationController: InterceptibleNavigationController(),
navigationController: InterceptableNavigationController(),
interactor: CustomListInteractor(repository: customListRepository),
tunnelManager: tunnelManager,
nodes: nodes
Expand Down

0 comments on commit 660b9df

Please sign in to comment.