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

View File

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

View File

@ -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
}
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()
if let query = Self.getSearchQuery(from: activity),
!query.isEmpty {
searchController.searchBar.text = query
resultsController.performSearch(query: query)
} else {
searchController.searchBar.becomeFirstResponder()
}
}
}