Use navigation sequencing for user activity handling
This commit is contained in:
parent
960ba84683
commit
cd8f0e7926
|
@ -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()()
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -136,10 +136,13 @@ 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
|
||||||
|
if let notificationsPageController = topViewController as? NotificationsPageViewController {
|
||||||
notificationsPageController.loadViewIfNeeded()
|
notificationsPageController.loadViewIfNeeded()
|
||||||
notificationsPageController.selectMode(Self.getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode)
|
notificationsPageController.selectMode(Self.getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode)
|
||||||
}
|
}
|
||||||
|
completion()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func getNotificationsMode(from activity: NSUserActivity) -> NotificationsMode? {
|
static func getNotificationsMode(from activity: NSUserActivity) -> NotificationsMode? {
|
||||||
|
@ -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
|
||||||
|
let pageController = topViewController as! TimelinesPageViewController
|
||||||
pageController.selectTimeline(pinned, animated: false)
|
pageController.selectTimeline(pinned, animated: false)
|
||||||
timelineVC = pageController.currentViewController as? TimelineViewController
|
}
|
||||||
} 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 {
|
||||||
|
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 {
|
Task {
|
||||||
await timelineVC.restoreStateFromHandoff(statusIDs: positionInfo.statusIDs, centerStatusID: positionInfo.centerStatusID)
|
await timelineVC.restoreStateFromHandoff(statusIDs: positionInfo.statusIDs, centerStatusID: positionInfo.centerStatusID)
|
||||||
|
completion()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -278,14 +290,15 @@ class UserActivityManager {
|
||||||
context.select(route: .explore)
|
context.select(route: .explore)
|
||||||
context.popToRoot()
|
context.popToRoot()
|
||||||
|
|
||||||
|
context.withTopViewController { topViewController, completion in
|
||||||
let searchController: UISearchController
|
let searchController: UISearchController
|
||||||
let resultsController: SearchResultsViewController
|
let resultsController: SearchResultsViewController
|
||||||
if let explore = context.topViewController as? ExploreViewController {
|
if let explore = topViewController as? ExploreViewController {
|
||||||
explore.loadViewIfNeeded()
|
explore.loadViewIfNeeded()
|
||||||
explore.searchControllerStatusOnAppearance = true
|
explore.searchControllerStatusOnAppearance = true
|
||||||
searchController = explore.searchController
|
searchController = explore.searchController
|
||||||
resultsController = explore.resultsController
|
resultsController = explore.resultsController
|
||||||
} else if let inlineTrends = context.topViewController as? InlineTrendsViewController {
|
} else if let inlineTrends = topViewController as? InlineTrendsViewController {
|
||||||
inlineTrends.loadViewIfNeeded()
|
inlineTrends.loadViewIfNeeded()
|
||||||
inlineTrends.searchControllerStatusOnAppearance = true
|
inlineTrends.searchControllerStatusOnAppearance = true
|
||||||
searchController = inlineTrends.searchController
|
searchController = inlineTrends.searchController
|
||||||
|
@ -302,6 +315,7 @@ class UserActivityManager {
|
||||||
searchController.searchBar.becomeFirstResponder()
|
searchController.searchBar.becomeFirstResponder()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static func bookmarksActivity(accountID: String) -> NSUserActivity {
|
static func bookmarksActivity(accountID: String) -> NSUserActivity {
|
||||||
let activity = NSUserActivity(type: .bookmarks, accountID: accountID)
|
let activity = NSUserActivity(type: .bookmarks, accountID: accountID)
|
||||||
|
|
Loading…
Reference in New Issue