From 0f2a85b1088cd7d8a27924b37715c465c2a52420 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 2 Jun 2024 10:44:31 -0700 Subject: [PATCH] Fix crash when opening push notification while VC modally presented The dismissal of the modally presented VC turns the route change into an asynchronous operation, even when not animated. Closes #484 --- Tusker/AppDelegate.swift | 7 ++- Tusker/Scenes/MainSceneDelegate.swift | 10 +++- ...ountSwitchingContainerViewController.swift | 14 +++-- Tusker/Screens/Main/Duckable+Root.swift | 4 +- .../Main/MainSplitViewController.swift | 7 ++- .../Main/MainTabBarViewController.swift | 3 +- .../Main/TuskerRootViewController.swift | 56 +------------------ Tusker/Shortcuts/AppShortcutItems.swift | 4 +- .../Shortcuts/NSUserActivity+Extensions.swift | 4 +- .../UserActivityHandlingContext.swift | 25 +++++++-- Tusker/Shortcuts/UserActivityManager.swift | 36 ++++++------ Tusker/Shortcuts/UserActivityType.swift | 2 +- 12 files changed, 70 insertions(+), 102 deletions(-) diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index f33ee29e..c5da7928 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -290,9 +290,10 @@ extension AppDelegate: UNUserNotificationCenterDelegate { // if the scene is already active, then we animate the account switching if necessary delegate.activateAccount(account, animated: scene.activationState == .foregroundActive) - rootViewController.select(route: .notifications, animated: false) - let vc = NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController) - rootViewController.getNavigationController().pushViewController(vc, animated: false) + rootViewController.select(route: .notifications, animated: false) { + let vc = NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController) + rootViewController.getNavigationController().pushViewController(vc, animated: false) + } } else { let activity = UserActivityManager.showNotificationActivity(id: notificationID, accountID: accountID) if #available(iOS 17.0, *) { diff --git a/Tusker/Scenes/MainSceneDelegate.swift b/Tusker/Scenes/MainSceneDelegate.swift index 3eddbb75..7b0966a7 100644 --- a/Tusker/Scenes/MainSceneDelegate.swift +++ b/Tusker/Scenes/MainSceneDelegate.swift @@ -83,7 +83,9 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate } else { context = ActiveAccountUserActivityHandlingContext(isHandoff: true, root: rootViewController!) } - _ = userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene, context: context)) + Task(priority: .userInitiated) { + _ = await userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene, context: context)) + } } func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { @@ -191,8 +193,10 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate if let activity = launchActivity { func doRestoreActivity(context: UserActivityHandlingContext) { - _ = activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!, context: context)) - context.finalize(activity: activity) + Task(priority: .userInitiated) { + _ = await activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!, context: context)) + context.finalize(activity: activity) + } } if activity.isStateRestorationActivity { doRestoreActivity(context: StateRestorationUserActivityHandlingContext(root: rootViewController!)) diff --git a/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift b/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift index fa6d57cb..6f6aede0 100644 --- a/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift +++ b/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift @@ -70,10 +70,12 @@ class AccountSwitchingContainerViewController: UIViewController { stateRestorationLogger.debug("AccountSwitchingContainer: reusing existing VC for \(account.id, privacy: .public)") } else { newRoot = newRootProvider() - stateRestorationLogger.debug("AccountSwitchingContainer: restoring \(activity.activityType, privacy: .public) for \(account.id, privacy: .public)") - let context = StateRestorationUserActivityHandlingContext(root: newRoot) - _ = activity.handleResume(manager: UserActivityManager(scene: view.window!.windowScene!, context: context)) - context.finalize(activity: activity) + Task(priority: .userInitiated) { + stateRestorationLogger.debug("AccountSwitchingContainer: restoring \(activity.activityType, privacy: .public) for \(account.id, privacy: .public)") + let context = StateRestorationUserActivityHandlingContext(root: newRoot) + _ = await activity.handleResume(manager: UserActivityManager(scene: view.window!.windowScene!, context: context)) + context.finalize(activity: activity) + } } } else { newRoot = newRootProvider() @@ -150,9 +152,9 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController { root.compose(editing: draft, animated: animated, isDucked: isDucked) } - func select(route: TuskerRoute, animated: Bool) { + func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) { loadViewIfNeeded() - root.select(route: route, animated: animated) + root.select(route: route, animated: animated, completion: completion) } func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? { diff --git a/Tusker/Screens/Main/Duckable+Root.swift b/Tusker/Screens/Main/Duckable+Root.swift index 875a08e8..893ce9e2 100644 --- a/Tusker/Screens/Main/Duckable+Root.swift +++ b/Tusker/Screens/Main/Duckable+Root.swift @@ -35,8 +35,8 @@ extension DuckableContainerViewController: AccountSwitchableViewController { (child as! TuskerRootViewController).getNavigationController() } - func select(route: TuskerRoute, animated: Bool) { - (child as? TuskerRootViewController)?.select(route: route, animated: animated) + func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) { + (child as? TuskerRootViewController)?.select(route: route, animated: animated, completion: completion) } func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? { diff --git a/Tusker/Screens/Main/MainSplitViewController.swift b/Tusker/Screens/Main/MainSplitViewController.swift index 3a73b63f..f55e2dec 100644 --- a/Tusker/Screens/Main/MainSplitViewController.swift +++ b/Tusker/Screens/Main/MainSplitViewController.swift @@ -542,14 +542,14 @@ extension MainSplitViewController: StateRestorableViewController { } extension MainSplitViewController: TuskerRootViewController { - func select(route: TuskerRoute, animated: Bool) { + func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) { guard traitCollection.horizontalSizeClass != .compact else { - tabBarViewController?.select(route: route, animated: animated) + tabBarViewController?.select(route: route, animated: animated, completion: completion) return } guard presentedViewController == nil else { dismiss(animated: animated) { - self.select(route: route, animated: animated) + self.select(route: route, animated: animated, completion: completion) } return } @@ -575,6 +575,7 @@ extension MainSplitViewController: TuskerRootViewController { let oldItem = sidebar.selectedItem sidebar.select(item: item, animated: false) select(newItem: item, oldItem: oldItem) + completion?() } func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? { diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index 5f447cc2..2216542b 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -289,7 +289,7 @@ extension MainTabBarViewController: StateRestorableViewController { } extension MainTabBarViewController: TuskerRootViewController { - func select(route: TuskerRoute, animated: Bool) { + func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) { switch route { case .timelines: select(tab: .timelines, dismissPresented: true) @@ -310,6 +310,7 @@ extension MainTabBarViewController: TuskerRootViewController { nav.pushViewController(ListTimelineViewController(for: list, mastodonController: mastodonController), animated: animated) } } + completion?() } func getNavigationDelegate() -> TuskerNavigationDelegate? { diff --git a/Tusker/Screens/Main/TuskerRootViewController.swift b/Tusker/Screens/Main/TuskerRootViewController.swift index 7f12b7cc..4b3fc83e 100644 --- a/Tusker/Screens/Main/TuskerRootViewController.swift +++ b/Tusker/Screens/Main/TuskerRootViewController.swift @@ -12,7 +12,7 @@ import ComposeUI @MainActor protocol TuskerRootViewController: UIViewController, StateRestorableViewController, StatusBarTappableViewController { func compose(editing draft: Draft?, animated: Bool, isDucked: Bool) - func select(route: TuskerRoute, animated: Bool) + func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? func getNavigationDelegate() -> TuskerNavigationDelegate? func getNavigationController() -> NavigationControllerProtocol @@ -21,33 +21,6 @@ protocol TuskerRootViewController: UIViewController, StateRestorableViewControll func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController? } -//extension TuskerRootViewController { -// func select(route: NewRoute, animated: Bool) { -// doApply(components: route.components, animated: animated) -// } -// -// private func doApply(components: ArraySlice, animated: Bool) { -// guard let first = components.first else { -// return -// } -// doApply(component: first, animated: animated) { -// self.doApply(components: components.dropFirst(), animated: animated) -// } -// } -// -// private func doApply(component: RouteComponent, animated: Bool, completion: @escaping () -> Void) { -// switch component { -// case .topLevelItem(let rootRoute): -// select(route: rootRoute) -// completion() -// case .popToRoot: -// _ = getNavigationController().popToRootViewController(animated: animated) -// completion() -// case .push(<#T##(MastodonController) -> UIViewController#>) -// } -// } -//} - enum TuskerRoute { case timelines case notifications @@ -57,33 +30,6 @@ enum TuskerRoute { case list(id: String) } -//struct NewRoute: ExpressibleByArrayLiteral { -// let components: [RouteComponent] -// -// init(arrayLiteral elements: RouteComponent...) { -// self.components = elements -// } -// -// static var timelines: Self { [.topLevelItem(.timelines)] } -// static var explore: Self { [.topLevelItem(.explore)] } -// static var myProfile: Self { [.topLevelItem(.myProfile)] } -// static var bookmarks: Self { [.topLevelItem(.explore), .push({ BookmarksViewController(mastodonController: $0) })] } -// static func profile(accountID: String) -> Self { [.topLevelItem(.timelines), .push({ ProfileViewController(accountID: accountID, mastodonController: $0) })] } -//} -// -//enum RouteComponent { -// case topLevelItem(RootRoute) -// case popToRoot -// case push((MastodonController) -> UIViewController) -// case present(UIViewController) -//} -// -//enum RootRoute { -// case timelines -// case explore -// case myProfile -//} -// @MainActor protocol NavigationControllerProtocol: UIViewController { var viewControllers: [UIViewController] { get set } diff --git a/Tusker/Shortcuts/AppShortcutItems.swift b/Tusker/Shortcuts/AppShortcutItems.swift index 18b822f8..9a3a62fb 100644 --- a/Tusker/Shortcuts/AppShortcutItems.swift +++ b/Tusker/Shortcuts/AppShortcutItems.swift @@ -44,9 +44,9 @@ enum AppShortcutItem: String, CaseIterable { } switch self { case .showHomeTimeline: - root.select(route: .timelines, animated: false) + root.select(route: .timelines, animated: false, completion: nil) case .showNotifications: - root.select(route: .notifications, animated: false) + root.select(route: .notifications, animated: false, completion: nil) case .composePost: root.compose(editing: nil, animated: false, isDucked: false) } diff --git a/Tusker/Shortcuts/NSUserActivity+Extensions.swift b/Tusker/Shortcuts/NSUserActivity+Extensions.swift index 7a460a72..027211f3 100644 --- a/Tusker/Shortcuts/NSUserActivity+Extensions.swift +++ b/Tusker/Shortcuts/NSUserActivity+Extensions.swift @@ -43,9 +43,9 @@ extension NSUserActivity { } @MainActor - func handleResume(manager: UserActivityManager) -> Bool { + func handleResume(manager: UserActivityManager) async -> Bool { guard let type = UserActivityType(rawValue: activityType) else { return false } - type.handle(manager)(self) + await type.handle(manager)(self) return true } diff --git a/Tusker/Shortcuts/UserActivityHandlingContext.swift b/Tusker/Shortcuts/UserActivityHandlingContext.swift index 9fafb780..000a0793 100644 --- a/Tusker/Shortcuts/UserActivityHandlingContext.swift +++ b/Tusker/Shortcuts/UserActivityHandlingContext.swift @@ -16,7 +16,8 @@ import ComposeUI protocol UserActivityHandlingContext { var isHandoff: Bool { get } - func select(route: TuskerRoute) + func select(route: TuskerRoute) async + func select(route: TuskerRoute, completion: (() -> Void)?) func present(_ vc: UIViewController) var topViewController: UIViewController? { get } @@ -28,6 +29,16 @@ protocol UserActivityHandlingContext { func finalize(activity: NSUserActivity) } +extension UserActivityHandlingContext { + func select(route: TuskerRoute) async { + await withCheckedContinuation { continuation in + select(route: route) { + continuation.resume() + } + } + } +} + struct ActiveAccountUserActivityHandlingContext: UserActivityHandlingContext { let isHandoff: Bool let root: TuskerRootViewController @@ -35,8 +46,8 @@ struct ActiveAccountUserActivityHandlingContext: UserActivityHandlingContext { root.getNavigationDelegate()! } - func select(route: TuskerRoute) { - root.select(route: route, animated: true) + func select(route: TuskerRoute, completion: (() -> Void)?) { + root.select(route: route, animated: true, completion: completion) } func present(_ vc: UIViewController) { @@ -71,9 +82,11 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext { var isHandoff: Bool { false } - func select(route: TuskerRoute) { - root.select(route: route, animated: false) - state = .selectedRoute + func select(route: TuskerRoute, completion: (() -> Void)?) { + root.select(route: route, animated: false) { + self.state = .selectedRoute + completion?() + } } var topViewController: UIViewController? { root.getNavigationController().topViewController } diff --git a/Tusker/Shortcuts/UserActivityManager.swift b/Tusker/Shortcuts/UserActivityManager.swift index 18b7fbd1..9a018447 100644 --- a/Tusker/Shortcuts/UserActivityManager.swift +++ b/Tusker/Shortcuts/UserActivityManager.swift @@ -133,8 +133,8 @@ class UserActivityManager { return activity } - func handleCheckNotifications(activity: NSUserActivity) { - context.select(route: .notifications) + func handleCheckNotifications(activity: NSUserActivity) async { + await context.select(route: .notifications) context.popToRoot() if let notificationsPageController = context.topViewController as? NotificationsPageViewController { notificationsPageController.loadViewIfNeeded() @@ -204,22 +204,22 @@ class UserActivityManager { return (timeline, positionInfo) } - func handleShowTimeline(activity: NSUserActivity) { + func handleShowTimeline(activity: NSUserActivity) async { 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) + await context.select(route: .timelines) context.popToRoot() let pageController = context.topViewController as! TimelinesPageViewController pageController.selectTimeline(pinned, animated: false) timelineVC = pageController.currentViewController as? TimelineViewController } else if case .list(let id) = timeline { - context.select(route: .list(id: id)) + await context.select(route: .list(id: id)) timelineVC = context.topViewController as? TimelineViewController } else { - context.select(route: .explore) + await context.select(route: .explore) context.popToRoot() timelineVC = TimelineViewController(for: timeline, mastodonController: mastodonController) context.push(timelineVC!) @@ -249,11 +249,11 @@ class UserActivityManager { return activity.userInfo?["mainStatusID"] as? String } - func handleShowConversation(activity: NSUserActivity) { + func handleShowConversation(activity: NSUserActivity) async { guard let mainStatusID = Self.getConversationStatus(from: activity) else { return } - context.select(route: .timelines) + await context.select(route: .timelines) context.push(ConversationViewController(for: mainStatusID, state: .unknown, mastodonController: mastodonController)) } @@ -274,8 +274,8 @@ class UserActivityManager { return activity.userInfo?["query"] as? String } - func handleSearch(activity: NSUserActivity) { - context.select(route: .explore) + func handleSearch(activity: NSUserActivity) async { + await context.select(route: .explore) context.popToRoot() let searchController: UISearchController @@ -311,8 +311,8 @@ class UserActivityManager { return activity } - func handleBookmarks(activity: NSUserActivity) { - context.select(route: .bookmarks) + func handleBookmarks(activity: NSUserActivity) async { + await context.select(route: .bookmarks) } // MARK: - My Profile @@ -325,8 +325,8 @@ class UserActivityManager { return activity } - func handleMyProfile(activity: NSUserActivity) { - context.select(route: .myProfile) + func handleMyProfile(activity: NSUserActivity) async { + await context.select(route: .myProfile) } // MARK: - Show Profile @@ -344,11 +344,11 @@ class UserActivityManager { return activity.userInfo?["profileID"] as? String } - func handleShowProfile(activity: NSUserActivity) { + func handleShowProfile(activity: NSUserActivity) async { guard let accountID = Self.getProfile(from: activity) else { return } - context.select(route: .timelines) + await context.select(route: .timelines) context.push(ProfileViewController(accountID: accountID, mastodonController: mastodonController)) } @@ -361,11 +361,11 @@ class UserActivityManager { return activity } - func handleShowNotification(activity: NSUserActivity) { + func handleShowNotification(activity: NSUserActivity) async { guard let notificationID = activity.userInfo?["notificationID"] as? String else { return } - context.select(route: .notifications) + await context.select(route: .notifications) context.push(NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController)) } diff --git a/Tusker/Shortcuts/UserActivityType.swift b/Tusker/Shortcuts/UserActivityType.swift index 6d7e991f..090cf8c2 100644 --- a/Tusker/Shortcuts/UserActivityType.swift +++ b/Tusker/Shortcuts/UserActivityType.swift @@ -23,7 +23,7 @@ enum UserActivityType: String { extension UserActivityType { @MainActor - var handle: (UserActivityManager) -> @MainActor (NSUserActivity) -> Void { + var handle: (UserActivityManager) -> @MainActor (NSUserActivity) async -> Void { switch self { case .mainScene: fatalError("cannot handle main scene activity")