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 15, 2024
1 parent f0faaaa commit 109273a
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 102 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

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,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)
}
}

Expand Down Expand Up @@ -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
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 All @@ -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: []))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 109273a

Please sign in to comment.