diff --git a/Tusker/Screens/Main/TuskerRootViewController.swift b/Tusker/Screens/Main/TuskerRootViewController.swift index e58a1d58..c779648c 100644 --- a/Tusker/Screens/Main/TuskerRootViewController.swift +++ b/Tusker/Screens/Main/TuskerRootViewController.swift @@ -42,8 +42,8 @@ enum TuskerRoute { /// 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 let root: any TuskerRootViewController + private let animated: Bool private var operations = [() -> Void]() init(root: any TuskerRootViewController, animated: Bool) { @@ -73,6 +73,24 @@ final class TuskerNavigationSequence { } } + func present(viewController: UIViewController) { + operations.append { + self.root.present(viewController, animated: self.animated, completion: self.run) + } + } + + func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void) { + operations.append { + block(self.root.getNavigationController().topViewController, self.run) + } + } + + func addOperation(_ operation: @escaping (_ completion: @escaping () -> Void) -> Void) { + operations.append { + operation(self.run) + } + } + func run() { if !operations.isEmpty { operations.removeFirst()() diff --git a/Tusker/Shortcuts/UserActivityHandlingContext.swift b/Tusker/Shortcuts/UserActivityHandlingContext.swift index e4ae550d..5903bb8e 100644 --- a/Tusker/Shortcuts/UserActivityHandlingContext.swift +++ b/Tusker/Shortcuts/UserActivityHandlingContext.swift @@ -17,12 +17,11 @@ protocol UserActivityHandlingContext { var isHandoff: Bool { get } func select(route: TuskerRoute) - func present(_ vc: UIViewController) - - var topViewController: UIViewController? { get } func popToRoot() func push(_ vc: UIViewController) - + func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void) + + func present(_ vc: UIViewController) func compose(editing draft: Draft) func finalize(activity: NSUserActivity) @@ -30,66 +29,81 @@ protocol UserActivityHandlingContext { struct ActiveAccountUserActivityHandlingContext: UserActivityHandlingContext { let isHandoff: Bool - let root: TuskerRootViewController - var navigationDelegate: TuskerNavigationDelegate { - root.getNavigationDelegate()! + private let root: TuskerRootViewController + private let navigation: TuskerNavigationSequence + + init(isHandoff: Bool, root: TuskerRootViewController) { + self.isHandoff = isHandoff + self.root = root + self.navigation = TuskerNavigationSequence(root: root, animated: true) } func select(route: TuskerRoute) { - root.select(route: route, animated: true, completion: nil) + navigation.select(route: route) } func present(_ vc: UIViewController) { - navigationDelegate.present(vc, animated: true) + navigation.present(viewController: vc) } - var topViewController: UIViewController? { root.getNavigationController().topViewController } - func popToRoot() { - _ = root.getNavigationController().popToRootViewController(animated: true) + navigation.popToRoot() } func push(_ vc: UIViewController) { - navigationDelegate.show(vc, sender: nil) + navigation.push(viewController: vc) } + func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void) { + navigation.withTopViewController(block) + } + func compose(editing draft: Draft) { - navigationDelegate.compose(editing: draft, animated: true, isDucked: true) + navigation.addOperation { completion in + root.compose(editing: draft, animated: true, isDucked: true, completion: completion) + } } func finalize(activity: NSUserActivity) { + navigation.run() } } class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext { private var state = State.initial - let root: TuskerRootViewController + private let root: TuskerRootViewController + private let navigation: TuskerNavigationSequence init(root: TuskerRootViewController) { self.root = root + self.navigation = TuskerNavigationSequence(root: root, animated: false) } - var isHandoff: Bool { false } + var isHandoff: Bool { + false + } func select(route: TuskerRoute) { - root.select(route: route, animated: false, completion: nil) + navigation.select(route: route) state = .selectedRoute } - var topViewController: UIViewController? { root.getNavigationController().topViewController } - func popToRoot() { - // unnecessary during state restoration + navigation.popToRoot() } func push(_ vc: UIViewController) { precondition(state >= .selectedRoute) - root.getNavigationController().pushViewController(vc, animated: false) + navigation.push(viewController: vc) state = .pushed } + func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void) { + navigation.withTopViewController(block) + } + func present(_ vc: UIViewController) { - root.present(vc, animated: false) + navigation.present(viewController: vc) state = .presented } @@ -107,6 +121,7 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext { func finalize(activity: NSUserActivity) { precondition(state > .initial) + navigation.run() #if !os(visionOS) if #available(iOS 16.0, *), let duckedDraft = UserActivityManager.getDuckedDraft(from: activity) { diff --git a/Tusker/Shortcuts/UserActivityManager.swift b/Tusker/Shortcuts/UserActivityManager.swift index 18b7fbd1..350f9774 100644 --- a/Tusker/Shortcuts/UserActivityManager.swift +++ b/Tusker/Shortcuts/UserActivityManager.swift @@ -136,9 +136,12 @@ class UserActivityManager { func handleCheckNotifications(activity: NSUserActivity) { context.select(route: .notifications) context.popToRoot() - if let notificationsPageController = context.topViewController as? NotificationsPageViewController { - notificationsPageController.loadViewIfNeeded() - notificationsPageController.selectMode(Self.getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode) + context.withTopViewController { topViewController, completion in + if let notificationsPageController = topViewController as? NotificationsPageViewController { + notificationsPageController.loadViewIfNeeded() + notificationsPageController.selectMode(Self.getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode) + } + completion() } } @@ -207,29 +210,38 @@ class UserActivityManager { func handleShowTimeline(activity: NSUserActivity) { guard let (timeline, positionInfo) = Self.getTimeline(from: activity) else { return } - var timelineVC: TimelineViewController? if let pinned = PinnedTimeline(timeline: timeline), mastodonController.accountPreferences.pinnedTimelines.contains(pinned) { context.select(route: .timelines) context.popToRoot() - let pageController = context.topViewController as! TimelinesPageViewController - pageController.selectTimeline(pinned, animated: false) - timelineVC = pageController.currentViewController as? TimelineViewController + context.withTopViewController { topViewController, completion in + let pageController = topViewController as! TimelinesPageViewController + pageController.selectTimeline(pinned, animated: false) + } } else if case .list(let id) = timeline { context.select(route: .list(id: id)) - timelineVC = context.topViewController as? TimelineViewController } else { context.select(route: .explore) context.popToRoot() - timelineVC = TimelineViewController(for: timeline, mastodonController: mastodonController) - context.push(timelineVC!) + let timelineVC = TimelineViewController(for: timeline, mastodonController: mastodonController) + context.push(timelineVC) } - if let timelineVC, - let positionInfo, + if let positionInfo, context.isHandoff { - Task { - await timelineVC.restoreStateFromHandoff(statusIDs: positionInfo.statusIDs, centerStatusID: positionInfo.centerStatusID) + context.withTopViewController { topViewController, completion in + let timelineVC: TimelineViewController + if let topViewController = topViewController as? TimelineViewController { + timelineVC = topViewController + } else if let topViewController = topViewController as? TimelinesPageViewController { + timelineVC = topViewController.currentViewController as! TimelineViewController + } else { + return + } + Task { + await timelineVC.restoreStateFromHandoff(statusIDs: positionInfo.statusIDs, centerStatusID: positionInfo.centerStatusID) + completion() + } } } } @@ -278,28 +290,30 @@ class UserActivityManager { context.select(route: .explore) context.popToRoot() - let searchController: UISearchController - let resultsController: SearchResultsViewController - if let explore = context.topViewController as? ExploreViewController { - explore.loadViewIfNeeded() - explore.searchControllerStatusOnAppearance = true - searchController = explore.searchController - resultsController = explore.resultsController - } else if let inlineTrends = context.topViewController as? InlineTrendsViewController { - inlineTrends.loadViewIfNeeded() - inlineTrends.searchControllerStatusOnAppearance = true - searchController = inlineTrends.searchController - resultsController = inlineTrends.resultsController - } else { - return - } - - if let query = Self.getSearchQuery(from: activity), - !query.isEmpty { - searchController.searchBar.text = query - resultsController.performSearch(query: query) - } else { - searchController.searchBar.becomeFirstResponder() + context.withTopViewController { topViewController, completion in + let searchController: UISearchController + let resultsController: SearchResultsViewController + if let explore = topViewController as? ExploreViewController { + explore.loadViewIfNeeded() + explore.searchControllerStatusOnAppearance = true + searchController = explore.searchController + resultsController = explore.resultsController + } else if let inlineTrends = topViewController as? InlineTrendsViewController { + inlineTrends.loadViewIfNeeded() + inlineTrends.searchControllerStatusOnAppearance = true + searchController = inlineTrends.searchController + resultsController = inlineTrends.resultsController + } else { + return + } + + if let query = Self.getSearchQuery(from: activity), + !query.isEmpty { + searchController.searchBar.text = query + resultsController.performSearch(query: query) + } else { + searchController.searchBar.becomeFirstResponder() + } } }