New way of sequencing navigation operations

Better fix for #484
This commit is contained in:
Shadowfacts 2024-08-22 14:32:01 -04:00
parent 2eead1f9de
commit 960ba84683
8 changed files with 90 additions and 32 deletions

View File

@ -292,12 +292,16 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
let rootViewController = delegate.rootViewController { let rootViewController = delegate.rootViewController {
let mastodonController = MastodonController.getForAccount(account) let mastodonController = MastodonController.getForAccount(account)
// if the scene is already active, then we animate the account switching if necessary // if the scene is already active, then we animate things
delegate.activateAccount(account, animated: scene.activationState == .foregroundActive) let animated = scene.activationState == .foregroundActive
rootViewController.select(route: .notifications, animated: false) delegate.activateAccount(account, animated: animated)
rootViewController.runNavigation(animated: animated) { navigation in
navigation.select(route: .notifications)
let vc = NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController) let vc = NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController)
rootViewController.getNavigationController().pushViewController(vc, animated: false) navigation.push(viewController: vc)
}
} else { } else {
let activity = UserActivityManager.showNotificationActivity(id: notificationID, accountID: accountID) let activity = UserActivityManager.showNotificationActivity(id: notificationID, accountID: accountID)
if #available(iOS 17.0, *) { if #available(iOS 17.0, *) {

View File

@ -159,9 +159,9 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController {
root.compose(editing: draft, animated: animated, isDucked: isDucked, completion: completion) root.compose(editing: draft, animated: animated, isDucked: isDucked, completion: completion)
} }
func select(route: TuskerRoute, animated: Bool) { func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
loadViewIfNeeded() loadViewIfNeeded()
root.select(route: route, animated: animated) root.select(route: route, animated: animated, completion: completion)
} }
func getNavigationDelegate() -> TuskerNavigationDelegate? { func getNavigationDelegate() -> TuskerNavigationDelegate? {

View File

@ -35,8 +35,8 @@ extension DuckableContainerViewController: AccountSwitchableViewController {
(child as! TuskerRootViewController).getNavigationController() (child as! TuskerRootViewController).getNavigationController()
} }
func select(route: TuskerRoute, animated: Bool) { func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
(child as? TuskerRootViewController)?.select(route: route, animated: animated) (child as? TuskerRootViewController)?.select(route: route, animated: animated, completion: completion)
} }
func performSearch(query: String) { func performSearch(query: String) {

View File

@ -333,14 +333,14 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
// Transfer the navigation stack, dropping the search VC, to keep anything the user has opened // Transfer the navigation stack, dropping the search VC, to keep anything the user has opened
transferNavigationStack(from: .tab(.explore), to: exploreNav, dropFirst: true, append: true) transferNavigationStack(from: .tab(.explore), to: exploreNav, dropFirst: true, append: true)
tabBarViewController.select(tab: .explore, dismissPresented: false) tabBarViewController.select(tab: .explore, dismissPresented: false, animated: false)
case let .tab(tab): case let .tab(tab):
// sidebar items that map 1 <-> 1 can be transferred directly // sidebar items that map 1 <-> 1 can be transferred directly
tabBarViewController.select(tab: tab, dismissPresented: false) tabBarViewController.select(tab: tab, dismissPresented: false, animated: false)
case .bookmarks, .favorites, .list(_), .savedHashtag(_), .savedInstance(_): case .bookmarks, .favorites, .list(_), .savedHashtag(_), .savedInstance(_):
tabBarViewController.select(tab: .explore, dismissPresented: false) tabBarViewController.select(tab: .explore, dismissPresented: false, animated: false)
// Make sure the Explore VC doesn't show its search bar when it appears, in case the user was previously // Make sure the Explore VC doesn't show its search bar when it appears, in case the user was previously
// in compact mode and performing a search. // in compact mode and performing a search.
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
@ -546,14 +546,14 @@ extension MainSplitViewController: StateRestorableViewController {
} }
extension MainSplitViewController: TuskerRootViewController { extension MainSplitViewController: TuskerRootViewController {
func select(route: TuskerRoute, animated: Bool) { func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
guard traitCollection.horizontalSizeClass != .compact else { guard traitCollection.horizontalSizeClass != .compact else {
tabBarViewController?.select(route: route, animated: animated) tabBarViewController?.select(route: route, animated: animated, completion: completion)
return return
} }
guard presentedViewController == nil else { guard presentedViewController == nil else {
dismiss(animated: animated) { dismiss(animated: animated) {
self.select(route: route, animated: animated) self.select(route: route, animated: animated, completion: completion)
} }
return return
} }
@ -579,6 +579,7 @@ extension MainSplitViewController: TuskerRootViewController {
let oldItem = sidebar.selectedItem let oldItem = sidebar.selectedItem
sidebar.select(item: item, animated: false) sidebar.select(item: item, animated: false)
select(newItem: item, oldItem: oldItem) select(newItem: item, oldItem: oldItem)
completion?()
} }
func getNavigationDelegate() -> TuskerNavigationDelegate? { func getNavigationDelegate() -> TuskerNavigationDelegate? {

View File

@ -54,19 +54,22 @@ class MainTabBarViewController: BaseMainTabBarViewController {
view.backgroundColor = .appBackground view.backgroundColor = .appBackground
} }
func select(tab: Tab, dismissPresented: Bool) { func select(tab: Tab, dismissPresented: Bool, animated: Bool, completion: (() -> Void)? = nil) {
if tab == .compose { if tab == .compose {
compose(editing: nil) compose(editing: nil, completion: completion)
} else { } else {
// when switching tabs, dismiss the currently presented VC // when switching tabs, dismiss the currently presented VC
// otherwise the selected tab changes behind the presented VC // otherwise the selected tab changes behind the presented VC
if presentedViewController != nil && dismissPresented { if presentedViewController != nil && dismissPresented {
dismiss(animated: true) { dismiss(animated: animated) {
stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)")
self.selectedIndex = tab.rawValue self.selectedIndex = tab.rawValue
completion?()
} }
} else { } else {
stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)") stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)")
selectedIndex = tab.rawValue selectedIndex = tab.rawValue
completion?()
} }
} }
} }
@ -148,21 +151,21 @@ extension MainTabBarViewController: UITabBarControllerDelegate {
} }
extension MainTabBarViewController: TuskerRootViewController { extension MainTabBarViewController: TuskerRootViewController {
func select(route: TuskerRoute, animated: Bool) { func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
switch route { switch route {
case .timelines: case .timelines:
select(tab: .timelines, dismissPresented: true) select(tab: .timelines, dismissPresented: true, animated: animated, completion: completion)
case .notifications: case .notifications:
select(tab: .notifications, dismissPresented: true) select(tab: .notifications, dismissPresented: true, animated: animated, completion: completion)
case .myProfile: case .myProfile:
select(tab: .myProfile, dismissPresented: true) select(tab: .myProfile, dismissPresented: true, animated: animated, completion: completion)
case .explore: case .explore:
select(tab: .explore, dismissPresented: true) select(tab: .explore, dismissPresented: true, animated: animated, completion: completion)
case .bookmarks: case .bookmarks:
select(tab: .explore, dismissPresented: true) select(tab: .explore, dismissPresented: true, animated: animated, completion: completion)
getNavigationController().pushViewController(BookmarksViewController(mastodonController: mastodonController), animated: animated) getNavigationController().pushViewController(BookmarksViewController(mastodonController: mastodonController), animated: animated)
case .list(id: let id): case .list(id: let id):
select(tab: .explore, dismissPresented: true) select(tab: .explore, dismissPresented: true, animated: animated, completion: completion)
if let list = mastodonController.getCachedList(id: id) { if let list = mastodonController.getCachedList(id: id) {
let nav = getNavigationController() let nav = getNavigationController()
_ = nav.popToRootViewController(animated: animated) _ = nav.popToRootViewController(animated: animated)
@ -185,7 +188,7 @@ extension MainTabBarViewController: TuskerRootViewController {
return return
} }
select(tab: .explore, dismissPresented: true) select(tab: .explore, dismissPresented: true, animated: false)
exploreNavController.popToRootViewController(animated: false) exploreNavController.popToRootViewController(animated: false)
// setting searchController.isActive directly doesn't work until the view has loaded/appeared for the first time // setting searchController.isActive directly doesn't work until the view has loaded/appeared for the first time

View File

@ -12,8 +12,7 @@ import ComposeUI
@MainActor @MainActor
protocol TuskerRootViewController: UIViewController, StateRestorableViewController, StatusBarTappableViewController { protocol TuskerRootViewController: UIViewController, StateRestorableViewController, StatusBarTappableViewController {
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?) func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?)
func select(route: TuskerRoute, animated: Bool) func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?)
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController?
func getNavigationDelegate() -> TuskerNavigationDelegate? func getNavigationDelegate() -> TuskerNavigationDelegate?
func getNavigationController() -> NavigationControllerProtocol func getNavigationController() -> NavigationControllerProtocol
func performSearch(query: String) func performSearch(query: String)
@ -21,6 +20,14 @@ protocol TuskerRootViewController: UIViewController, StateRestorableViewControll
func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController? func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController?
} }
extension TuskerRootViewController {
func runNavigation(animated: Bool, _ builder: (_ navigation: TuskerNavigationSequence) -> Void) {
let sequence = TuskerNavigationSequence(root: self, animated: animated)
builder(sequence)
sequence.run()
}
}
enum TuskerRoute { enum TuskerRoute {
case timelines case timelines
case notifications case notifications
@ -30,6 +37,49 @@ enum TuskerRoute {
case list(id: String) case list(id: String)
} }
/// A class that manages running a sequence of navigation operations on a ``TuskerRootViewController``.
///
/// Use this type, rather than calling multiple methods on the root VC in a row, because it manages waiting until each previous step finishes.
@MainActor
final class TuskerNavigationSequence {
let root: any TuskerRootViewController
let animated: Bool
private var operations = [() -> Void]()
init(root: any TuskerRootViewController, animated: Bool) {
self.root = root
self.animated = animated
}
func select(route: TuskerRoute) {
operations.append {
self.root.select(route: route, animated: self.animated, completion: self.run)
}
}
func push(viewController: UIViewController) {
operations.append {
let nav = self.root.getNavigationController()
nav.pushViewController(viewController, animated: self.animated)
self.run()
}
}
func popToRoot() {
operations.append {
let nav = self.root.getNavigationController()
nav.popToRootViewController(animated: self.animated)
self.run()
}
}
func run() {
if !operations.isEmpty {
operations.removeFirst()()
}
}
}
@MainActor @MainActor
protocol NavigationControllerProtocol: UIViewController { protocol NavigationControllerProtocol: UIViewController {
var viewControllers: [UIViewController] { get set } var viewControllers: [UIViewController] { get set }

View File

@ -44,9 +44,9 @@ enum AppShortcutItem: String, CaseIterable {
} }
switch self { switch self {
case .showHomeTimeline: case .showHomeTimeline:
root.select(route: .timelines, animated: false) root.select(route: .timelines, animated: false, completion: nil)
case .showNotifications: case .showNotifications:
root.select(route: .notifications, animated: false) root.select(route: .notifications, animated: false, completion: nil)
case .composePost: case .composePost:
root.compose(editing: nil, animated: false, isDucked: false, completion: nil) root.compose(editing: nil, animated: false, isDucked: false, completion: nil)
} }

View File

@ -36,7 +36,7 @@ struct ActiveAccountUserActivityHandlingContext: UserActivityHandlingContext {
} }
func select(route: TuskerRoute) { func select(route: TuskerRoute) {
root.select(route: route, animated: true) root.select(route: route, animated: true, completion: nil)
} }
func present(_ vc: UIViewController) { func present(_ vc: UIViewController) {
@ -72,7 +72,7 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
var isHandoff: Bool { false } var isHandoff: Bool { false }
func select(route: TuskerRoute) { func select(route: TuskerRoute) {
root.select(route: route, animated: false) root.select(route: route, animated: false, completion: nil)
state = .selectedRoute state = .selectedRoute
} }