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
This commit is contained in:
Shadowfacts 2024-06-02 10:44:31 -07:00
parent 5e55ce75c2
commit 0f2a85b108
12 changed files with 70 additions and 102 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<RouteComponent>, 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 }

View File

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

View File

@ -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
}

View File

@ -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 }

View File

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

View File

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