Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Move tracking unsaved changes into coordinator
Browse files Browse the repository at this point in the history
mojganii authored and Jon Petersson committed Apr 15, 2024
1 parent 1b81d5f commit bf71ec9
Showing 6 changed files with 114 additions and 102 deletions.
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ class InterceptibleNavigationController: CustomNavigationController {

// 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 }

@@ -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)
Original file line number Diff line number Diff line change
@@ -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
}

@@ -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()

@@ -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
@@ -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
@@ -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,
@@ -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

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

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

guard let interceptibleNavigationController = navigationController as? InterceptibleNavigationController else {
return
}

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

presentUnsavedChangesDialog()
return false
}

interceptibleNavigationController.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)
}
}

@@ -90,3 +164,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
@@ -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)
}
@@ -84,6 +90,14 @@ class ListCustomListCoordinator: Coordinator, Presentable, Presenting {
customListSelection: UserSelectedRelays.CustomListSelection(listId: list.id, isList: true)
)
relayConstraints.locations = .only(selectedRelays)
} else {
let selectedConstraintIsRemovedFromList = list.locations.filter {
relayConstraints.locations.value?.locations.contains($0) ?? false
}.isEmpty

if selectedConstraintIsRemovedFromList {
relayConstraints.locations = .only(UserSelectedRelays(locations: []))
}
}
case .delete:
relayConstraints.locations = .only(UserSelectedRelays(locations: []))
Original file line number Diff line number Diff line change
@@ -15,13 +15,16 @@ struct LocationCellViewModel: Hashable {
var isSelected = false

func hash(into hasher: inout Hasher) {
hasher.combine(section)
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
Original file line number Diff line number Diff line change
@@ -185,7 +185,7 @@ final class LocationDataSource:

let rootNode = selectedItem.node.root

// Exit early if no changes to the node tree are necessary.
// Exit early if no changes to the node tree should be made.
guard selectedItem.node != rootNode else {
completion?()
return

0 comments on commit bf71ec9

Please sign in to comment.