Use navigation sequencing for user activity handling

This commit is contained in:
Shadowfacts 2024-08-22 14:49:27 -04:00
parent 960ba84683
commit cd8f0e7926
3 changed files with 107 additions and 60 deletions

View File

@ -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. /// 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 @MainActor
final class TuskerNavigationSequence { final class TuskerNavigationSequence {
let root: any TuskerRootViewController private let root: any TuskerRootViewController
let animated: Bool private let animated: Bool
private var operations = [() -> Void]() private var operations = [() -> Void]()
init(root: any TuskerRootViewController, animated: Bool) { 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() { func run() {
if !operations.isEmpty { if !operations.isEmpty {
operations.removeFirst()() operations.removeFirst()()

View File

@ -17,12 +17,11 @@ protocol UserActivityHandlingContext {
var isHandoff: Bool { get } var isHandoff: Bool { get }
func select(route: TuskerRoute) func select(route: TuskerRoute)
func present(_ vc: UIViewController)
var topViewController: UIViewController? { get }
func popToRoot() func popToRoot()
func push(_ vc: UIViewController) 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 compose(editing draft: Draft)
func finalize(activity: NSUserActivity) func finalize(activity: NSUserActivity)
@ -30,66 +29,81 @@ protocol UserActivityHandlingContext {
struct ActiveAccountUserActivityHandlingContext: UserActivityHandlingContext { struct ActiveAccountUserActivityHandlingContext: UserActivityHandlingContext {
let isHandoff: Bool let isHandoff: Bool
let root: TuskerRootViewController private let root: TuskerRootViewController
var navigationDelegate: TuskerNavigationDelegate { private let navigation: TuskerNavigationSequence
root.getNavigationDelegate()!
init(isHandoff: Bool, root: TuskerRootViewController) {
self.isHandoff = isHandoff
self.root = root
self.navigation = TuskerNavigationSequence(root: root, animated: true)
} }
func select(route: TuskerRoute) { func select(route: TuskerRoute) {
root.select(route: route, animated: true, completion: nil) navigation.select(route: route)
} }
func present(_ vc: UIViewController) { func present(_ vc: UIViewController) {
navigationDelegate.present(vc, animated: true) navigation.present(viewController: vc)
} }
var topViewController: UIViewController? { root.getNavigationController().topViewController }
func popToRoot() { func popToRoot() {
_ = root.getNavigationController().popToRootViewController(animated: true) navigation.popToRoot()
} }
func push(_ vc: UIViewController) { 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) { 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) { func finalize(activity: NSUserActivity) {
navigation.run()
} }
} }
class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext { class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
private var state = State.initial private var state = State.initial
let root: TuskerRootViewController private let root: TuskerRootViewController
private let navigation: TuskerNavigationSequence
init(root: TuskerRootViewController) { init(root: TuskerRootViewController) {
self.root = root self.root = root
self.navigation = TuskerNavigationSequence(root: root, animated: false)
} }
var isHandoff: Bool { false } var isHandoff: Bool {
false
}
func select(route: TuskerRoute) { func select(route: TuskerRoute) {
root.select(route: route, animated: false, completion: nil) navigation.select(route: route)
state = .selectedRoute state = .selectedRoute
} }
var topViewController: UIViewController? { root.getNavigationController().topViewController }
func popToRoot() { func popToRoot() {
// unnecessary during state restoration navigation.popToRoot()
} }
func push(_ vc: UIViewController) { func push(_ vc: UIViewController) {
precondition(state >= .selectedRoute) precondition(state >= .selectedRoute)
root.getNavigationController().pushViewController(vc, animated: false) navigation.push(viewController: vc)
state = .pushed state = .pushed
} }
func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void) {
navigation.withTopViewController(block)
}
func present(_ vc: UIViewController) { func present(_ vc: UIViewController) {
root.present(vc, animated: false) navigation.present(viewController: vc)
state = .presented state = .presented
} }
@ -107,6 +121,7 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
func finalize(activity: NSUserActivity) { func finalize(activity: NSUserActivity) {
precondition(state > .initial) precondition(state > .initial)
navigation.run()
#if !os(visionOS) #if !os(visionOS)
if #available(iOS 16.0, *), if #available(iOS 16.0, *),
let duckedDraft = UserActivityManager.getDuckedDraft(from: activity) { let duckedDraft = UserActivityManager.getDuckedDraft(from: activity) {

View File

@ -136,9 +136,12 @@ class UserActivityManager {
func handleCheckNotifications(activity: NSUserActivity) { func handleCheckNotifications(activity: NSUserActivity) {
context.select(route: .notifications) context.select(route: .notifications)
context.popToRoot() context.popToRoot()
if let notificationsPageController = context.topViewController as? NotificationsPageViewController { context.withTopViewController { topViewController, completion in
notificationsPageController.loadViewIfNeeded() if let notificationsPageController = topViewController as? NotificationsPageViewController {
notificationsPageController.selectMode(Self.getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode) notificationsPageController.loadViewIfNeeded()
notificationsPageController.selectMode(Self.getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode)
}
completion()
} }
} }
@ -207,29 +210,38 @@ class UserActivityManager {
func handleShowTimeline(activity: NSUserActivity) { func handleShowTimeline(activity: NSUserActivity) {
guard let (timeline, positionInfo) = Self.getTimeline(from: activity) else { return } guard let (timeline, positionInfo) = Self.getTimeline(from: activity) else { return }
var timelineVC: TimelineViewController?
if let pinned = PinnedTimeline(timeline: timeline), if let pinned = PinnedTimeline(timeline: timeline),
mastodonController.accountPreferences.pinnedTimelines.contains(pinned) { mastodonController.accountPreferences.pinnedTimelines.contains(pinned) {
context.select(route: .timelines) context.select(route: .timelines)
context.popToRoot() context.popToRoot()
let pageController = context.topViewController as! TimelinesPageViewController context.withTopViewController { topViewController, completion in
pageController.selectTimeline(pinned, animated: false) let pageController = topViewController as! TimelinesPageViewController
timelineVC = pageController.currentViewController as? TimelineViewController pageController.selectTimeline(pinned, animated: false)
}
} else if case .list(let id) = timeline { } else if case .list(let id) = timeline {
context.select(route: .list(id: id)) context.select(route: .list(id: id))
timelineVC = context.topViewController as? TimelineViewController
} else { } else {
context.select(route: .explore) context.select(route: .explore)
context.popToRoot() context.popToRoot()
timelineVC = TimelineViewController(for: timeline, mastodonController: mastodonController) let timelineVC = TimelineViewController(for: timeline, mastodonController: mastodonController)
context.push(timelineVC!) context.push(timelineVC)
} }
if let timelineVC, if let positionInfo,
let positionInfo,
context.isHandoff { context.isHandoff {
Task { context.withTopViewController { topViewController, completion in
await timelineVC.restoreStateFromHandoff(statusIDs: positionInfo.statusIDs, centerStatusID: positionInfo.centerStatusID) 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.select(route: .explore)
context.popToRoot() context.popToRoot()
let searchController: UISearchController context.withTopViewController { topViewController, completion in
let resultsController: SearchResultsViewController let searchController: UISearchController
if let explore = context.topViewController as? ExploreViewController { let resultsController: SearchResultsViewController
explore.loadViewIfNeeded() if let explore = topViewController as? ExploreViewController {
explore.searchControllerStatusOnAppearance = true explore.loadViewIfNeeded()
searchController = explore.searchController explore.searchControllerStatusOnAppearance = true
resultsController = explore.resultsController searchController = explore.searchController
} else if let inlineTrends = context.topViewController as? InlineTrendsViewController { resultsController = explore.resultsController
inlineTrends.loadViewIfNeeded() } else if let inlineTrends = topViewController as? InlineTrendsViewController {
inlineTrends.searchControllerStatusOnAppearance = true inlineTrends.loadViewIfNeeded()
searchController = inlineTrends.searchController inlineTrends.searchControllerStatusOnAppearance = true
resultsController = inlineTrends.resultsController searchController = inlineTrends.searchController
} else { resultsController = inlineTrends.resultsController
return } else {
} return
}
if let query = Self.getSearchQuery(from: activity), if let query = Self.getSearchQuery(from: activity),
!query.isEmpty { !query.isEmpty {
searchController.searchBar.text = query searchController.searchBar.text = query
resultsController.performSearch(query: query) resultsController.performSearch(query: query)
} else { } else {
searchController.searchBar.becomeFirstResponder() searchController.searchBar.becomeFirstResponder()
}
} }
} }