Fix user activities not continuing when passed at launch

Fix crash when continuing user activities on iPad
This commit is contained in:
Shadowfacts 2022-05-13 17:10:18 -04:00
parent a2868739c2
commit cc0da2ec54
17 changed files with 101 additions and 28 deletions

View File

@ -9,6 +9,9 @@
import UIKit import UIKit
import CrashReporter import CrashReporter
import CoreData import CoreData
import OSLog
let stateRestorationLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "StateRestoration")
@UIApplicationMain @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
@ -85,7 +88,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
switch type { switch type {
case .mainScene: case .mainScene:
return "main-scene" return "main-scene"
case .showConversation, case .showConversation,
.showTimeline, .showTimeline,
.checkNotifications, .checkNotifications,
@ -93,7 +96,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
.bookmarks, .bookmarks,
.myProfile, .myProfile,
.showProfile: .showProfile:
return "auxiliary" if activity.displaysAuxiliaryScene {
stateRestorationLogger.info("Using auxiliary scene for \(type.rawValue, privacy: .public)")
return "auxiliary"
} else {
return "main-scene"
}
case .newPost: case .newPost:
return "compose" return "compose"

View File

@ -28,6 +28,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity { if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
launchActivity = activity launchActivity = activity
} }
stateRestorationLogger.info("MainSceneDelegate.launchActivity = \(self.launchActivity?.activityType ?? "nil", privacy: .public)")
window = UIWindow(windowScene: windowScene) window = UIWindow(windowScene: windowScene)
@ -69,7 +70,8 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
} }
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
_ = userActivity.handleResume() stateRestorationLogger.info("MainSceneDelegate.scene(_:continue:) called with \(userActivity.activityType, privacy: .public)")
_ = userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene))
} }
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
@ -169,6 +171,11 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
} }
activateAccount(account, animated: false) activateAccount(account, animated: false)
if let activity = launchActivity,
activity.activityType != UserActivityType.mainScene.rawValue {
_ = activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!))
}
} else { } else {
window!.rootViewController = createOnboardingUI() window!.rootViewController = createOnboardingUI()
} }

View File

@ -123,6 +123,7 @@ extension DraftsTableViewController: UITableViewDragDelegate {
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
let draft = self.draft(for: indexPath) let draft = self.draft(for: indexPath)
let activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: account.id) let activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: account.id)
activity.displaysAuxiliaryScene = true
let provider = NSItemProvider(object: activity) let provider = NSItemProvider(object: activity)
return [UIDragItem(itemProvider: provider)] return [UIDragItem(itemProvider: provider)]
} }

View File

@ -574,13 +574,17 @@ extension ExploreViewController: UICollectionViewDragDelegate {
let provider: NSItemProvider let provider: NSItemProvider
switch item { switch item {
case .bookmarks: case .bookmarks:
provider = NSItemProvider(object: UserActivityManager.bookmarksActivity()) let activity = UserActivityManager.bookmarksActivity()
activity.displaysAuxiliaryScene = true
provider = NSItemProvider(object: activity)
case let .list(list): case let .list(list):
guard let activity = UserActivityManager.showTimelineActivity(timeline: .list(id: list.id), accountID: accountID) else { return [] } guard let activity = UserActivityManager.showTimelineActivity(timeline: .list(id: list.id), accountID: accountID) else { return [] }
activity.displaysAuxiliaryScene = true
provider = NSItemProvider(object: activity) provider = NSItemProvider(object: activity)
case let .savedHashtag(hashtag): case let .savedHashtag(hashtag):
provider = NSItemProvider(object: hashtag.url as NSURL) provider = NSItemProvider(object: hashtag.url as NSURL)
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: accountID) { if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: accountID) {
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all) provider.registerObject(activity, visibility: .all)
} }
case let .savedInstance(url): case let .savedInstance(url):

View File

@ -183,6 +183,7 @@ extension ProfileDirectoryViewController: UICollectionViewDragDelegate {
} }
let provider = NSItemProvider(object: account.url as NSURL) let provider = NSItemProvider(object: account.url as NSURL)
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID) let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID)
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all) provider.registerObject(activity, visibility: .all)
return [UIDragItem(itemProvider: provider)] return [UIDragItem(itemProvider: provider)]
} }

View File

@ -90,6 +90,7 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
} }
let provider = NSItemProvider(object: hashtag.url as NSURL) let provider = NSItemProvider(object: hashtag.url as NSURL)
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) { if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all) provider.registerObject(activity, visibility: .all)
} }
return [UIDragItem(itemProvider: provider)] return [UIDragItem(itemProvider: provider)]

View File

@ -19,6 +19,11 @@ class MainSidebarViewController: UIViewController {
private weak var mastodonController: MastodonController! private weak var mastodonController: MastodonController!
weak var sidebarDelegate: MainSidebarViewControllerDelegate? weak var sidebarDelegate: MainSidebarViewControllerDelegate?
var onViewDidLoad: (() -> Void)? = nil {
willSet {
precondition(onViewDidLoad == nil)
}
}
private var collectionView: UICollectionView! private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
@ -95,6 +100,8 @@ class MainSidebarViewController: UIViewController {
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedHashtags), name: .savedHashtagsChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedHashtags), name: .savedHashtagsChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
onViewDidLoad?()
} }
func select(item: Item, animated: Bool) { func select(item: Item, animated: Bool) {
@ -553,6 +560,7 @@ extension MainSidebarViewController: UICollectionViewDelegate {
collectionView.contextMenuInteraction?.menuAppearance == .rich { collectionView.contextMenuInteraction?.menuAppearance == .rich {
return nil return nil
} }
activity.displaysAuxiliaryScene = true
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) in return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) in
return UIMenu(children: [ return UIMenu(children: [
UIWindowScene.ActivationAction({ action in UIWindowScene.ActivationAction({ action in
@ -573,6 +581,7 @@ extension MainSidebarViewController: UICollectionViewDragDelegate {
return [] return []
} }
activity.displaysAuxiliaryScene = true
let provider = NSItemProvider(object: activity) let provider = NSItemProvider(object: activity)
return [UIDragItem(itemProvider: provider)] return [UIDragItem(itemProvider: provider)]
} }

View File

@ -59,10 +59,15 @@ class MainSplitViewController: UISplitViewController {
switcher.itemOrientation = .iconsLeading switcher.itemOrientation = .iconsLeading
switcher.view.translatesAutoresizingMaskIntoConstraints = false switcher.view.translatesAutoresizingMaskIntoConstraints = false
switcher.delegate = self switcher.delegate = self
sidebar.view.addGestureRecognizer(switcher.createSwitcherGesture()) // accessing .view unconditionally loads the view, which we don't want to happen
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(sidebarTapped)) // because the sidebar view not being loaded is how we know not to transfer nav state
tapRecognizer.cancelsTouchesInView = false // in splitViewControllerDidCollapse on devices where the sidebar is never shown
sidebar.view.addGestureRecognizer(tapRecognizer) sidebar.onViewDidLoad = { [unowned self] in
self.sidebar.view.addGestureRecognizer(switcher.createSwitcherGesture())
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(sidebarTapped))
tapRecognizer.cancelsTouchesInView = false
self.sidebar.view.addGestureRecognizer(tapRecognizer)
}
} }
tabBarViewController = MainTabBarViewController(mastodonController: mastodonController) tabBarViewController = MainTabBarViewController(mastodonController: mastodonController)

View File

@ -42,6 +42,8 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
stateRestorationLogger.info("MainTabBarViewController: viewDidLoad, selectedIndex=\(self.selectedIndex, privacy: .public)")
self.delegate = self self.delegate = self
@ -75,6 +77,16 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
tabBar.isSpringLoaded = true tabBar.isSpringLoaded = true
} }
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
stateRestorationLogger.info("MainTabBarViewController: viewWillAppear, selectedIndex=\(self.selectedIndex, privacy: .public)")
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
stateRestorationLogger.info("MainTabBarViewController: viewDidAppear, selectedIndex=\(self.selectedIndex, privacy: .public)")
}
override func viewDidLayoutSubviews() { override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews() super.viewDidLayoutSubviews()
@ -240,6 +252,7 @@ extension MainTabBarViewController: TuskerRootViewController {
self.selectedIndex = tab.rawValue self.selectedIndex = tab.rawValue
} }
} else { } else {
stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)")
selectedIndex = tab.rawValue selectedIndex = tab.rawValue
} }
} }

View File

@ -10,13 +10,25 @@ import Foundation
extension NSUserActivity { extension NSUserActivity {
var displaysAuxiliaryScene: Bool {
get {
(userInfo?["displaysAuxiliaryScene"] as? Bool) ?? false
}
set {
if userInfo == nil {
userInfo = [:]
}
userInfo!["displaysAuxiliaryScene"] = newValue
}
}
convenience init(type: UserActivityType) { convenience init(type: UserActivityType) {
self.init(activityType: type.rawValue) self.init(activityType: type.rawValue)
} }
func handleResume() -> Bool { func handleResume(manager: UserActivityManager) -> Bool {
guard let type = UserActivityType(rawValue: activityType) else { return false } guard let type = UserActivityType(rawValue: activityType) else { return false }
type.handle(self) type.handle(manager)(self)
return true return true
} }

View File

@ -9,25 +9,32 @@
import UIKit import UIKit
import Intents import Intents
import Pachyderm import Pachyderm
import OSLog
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "UserActivityManager")
class UserActivityManager { class UserActivityManager {
private let scene: UIWindowScene
init(scene: UIWindowScene) {
self.scene = scene
}
// MARK: - Utils // MARK: - Utils
private static let encoder = PropertyListEncoder() private static let encoder = PropertyListEncoder()
private static let decoder = PropertyListDecoder() private static let decoder = PropertyListDecoder()
private static var mastodonController: MastodonController { private var mastodonController: MastodonController {
let scene = UIApplication.shared.activeOrBackgroundScene! scene.session.mastodonController!
return scene.session.mastodonController!
} }
private static func getMainViewController() -> TuskerRootViewController { private func getMainViewController() -> TuskerRootViewController {
let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first! let window = scene.windows.first { $0.isKeyWindow } ?? scene.windows.first!
let window = scene.windows.first { $0.isKeyWindow }!
return window.rootViewController as! TuskerRootViewController return window.rootViewController as! TuskerRootViewController
} }
private static func present(_ vc: UIViewController, animated: Bool = true) { private func present(_ vc: UIViewController, animated: Bool = true) {
getMainViewController().present(vc, animated: animated) getMainViewController().present(vc, animated: animated)
} }
@ -66,7 +73,7 @@ class UserActivityManager {
return activity return activity
} }
static func handleNewPost(activity: NSUserActivity) { func handleNewPost(activity: NSUserActivity) {
// TODO: check not currently showing compose screen // TODO: check not currently showing compose screen
let mentioning = activity.userInfo?["mentioning"] as? String let mentioning = activity.userInfo?["mentioning"] as? String
let draft = mastodonController.createDraft(mentioningAcct: mentioning) let draft = mastodonController.createDraft(mentioningAcct: mentioning)
@ -111,14 +118,14 @@ class UserActivityManager {
return activity return activity
} }
static func handleCheckNotifications(activity: NSUserActivity) { func handleCheckNotifications(activity: NSUserActivity) {
let mainViewController = getMainViewController() let mainViewController = getMainViewController()
mainViewController.select(tab: .notifications) mainViewController.select(tab: .notifications)
if let navigationController = mainViewController.getTabController(tab: .notifications) as? UINavigationController, if let navigationController = mainViewController.getTabController(tab: .notifications) as? UINavigationController,
let notificationsPageController = navigationController.viewControllers.first as? NotificationsPageViewController { let notificationsPageController = navigationController.viewControllers.first as? NotificationsPageViewController {
navigationController.popToRootViewController(animated: false) navigationController.popToRootViewController(animated: false)
notificationsPageController.loadViewIfNeeded() notificationsPageController.loadViewIfNeeded()
notificationsPageController.selectMode(getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode) notificationsPageController.selectMode(Self.getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode)
} }
} }
@ -168,11 +175,11 @@ class UserActivityManager {
let data = activity.userInfo?["timelineData"] as? Data else { let data = activity.userInfo?["timelineData"] as? Data else {
return nil return nil
} }
return try? decoder.decode(Timeline.self, from: data) return try? UserActivityManager.decoder.decode(Timeline.self, from: data)
} }
static func handleShowTimeline(activity: NSUserActivity) { func handleShowTimeline(activity: NSUserActivity) {
guard let timeline = getTimeline(from: activity) else { return } guard let timeline = Self.getTimeline(from: activity) else { return }
let mainViewController = getMainViewController() let mainViewController = getMainViewController()
mainViewController.select(tab: .timelines) mainViewController.select(tab: .timelines)
@ -228,7 +235,7 @@ class UserActivityManager {
return activity return activity
} }
static func handleSearch(activity: NSUserActivity) { func handleSearch(activity: NSUserActivity) {
let mainViewController = getMainViewController() let mainViewController = getMainViewController()
mainViewController.select(tab: .explore) mainViewController.select(tab: .explore)
if let navigationController = mainViewController.getTabController(tab: .explore) as? UINavigationController, if let navigationController = mainViewController.getTabController(tab: .explore) as? UINavigationController,
@ -247,7 +254,7 @@ class UserActivityManager {
return activity return activity
} }
static func handleBookmarks(activity: NSUserActivity) { func handleBookmarks(activity: NSUserActivity) {
let mainViewController = getMainViewController() let mainViewController = getMainViewController()
mainViewController.select(tab: .explore) mainViewController.select(tab: .explore)
if let navigationController = mainViewController.getTabController(tab: .explore) as? UINavigationController { if let navigationController = mainViewController.getTabController(tab: .explore) as? UINavigationController {
@ -265,7 +272,7 @@ class UserActivityManager {
return activity return activity
} }
static func handleMyProfile(activity: NSUserActivity) { func handleMyProfile(activity: NSUserActivity) {
let mainViewController = getMainViewController() let mainViewController = getMainViewController()
mainViewController.select(tab: .myProfile) mainViewController.select(tab: .myProfile)
} }

View File

@ -21,7 +21,7 @@ enum UserActivityType: String {
} }
extension UserActivityType { extension UserActivityType {
var handle: (NSUserActivity) -> Void { var handle: (UserActivityManager) -> (NSUserActivity) -> Void {
switch self { switch self {
case .mainScene: case .mainScene:
fatalError("cannot handle main scene activity") fatalError("cannot handle main scene activity")

View File

@ -115,6 +115,7 @@ extension AccountTableViewCell: DraggableTableViewCell {
} }
let provider = NSItemProvider(object: account.url as NSURL) let provider = NSItemProvider(object: account.url as NSURL)
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID) let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID)
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all) provider.registerObject(activity, visibility: .all)
return [UIDragItem(itemProvider: provider)] return [UIDragItem(itemProvider: provider)]
} }

View File

@ -223,6 +223,7 @@ extension FollowNotificationGroupTableViewCell: DraggableTableViewCell {
let notification = group.notifications[0] let notification = group.notifications[0]
let provider = NSItemProvider(object: notification.account.url as NSURL) let provider = NSItemProvider(object: notification.account.url as NSURL)
let activity = UserActivityManager.showProfileActivity(id: notification.account.id, accountID: mastodonController.accountInfo!.id) let activity = UserActivityManager.showProfileActivity(id: notification.account.id, accountID: mastodonController.accountInfo!.id)
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all) provider.registerObject(activity, visibility: .all)
return [UIDragItem(itemProvider: provider)] return [UIDragItem(itemProvider: provider)]
} }

View File

@ -206,6 +206,7 @@ extension FollowRequestNotificationTableViewCell: DraggableTableViewCell {
func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] { func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] {
let provider = NSItemProvider(object: account.url as NSURL) let provider = NSItemProvider(object: account.url as NSURL)
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: mastodonController.accountInfo!.id) let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: mastodonController.accountInfo!.id)
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all) provider.registerObject(activity, visibility: .all)
return [UIDragItem(itemProvider: provider)] return [UIDragItem(itemProvider: provider)]
} }

View File

@ -531,6 +531,7 @@ extension BaseStatusTableViewCell: UIDragInteractionDelegate {
} }
let provider = NSItemProvider(object: account.url as NSURL) let provider = NSItemProvider(object: account.url as NSURL)
let activity = UserActivityManager.showProfileActivity(id: accountID, accountID: currentAccountID) let activity = UserActivityManager.showProfileActivity(id: accountID, accountID: currentAccountID)
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all) provider.registerObject(activity, visibility: .all)
return [UIDragItem(itemProvider: provider)] return [UIDragItem(itemProvider: provider)]
} }

View File

@ -356,6 +356,7 @@ extension TimelineStatusTableViewCell: DraggableTableViewCell {
} }
let provider = NSItemProvider(object: status.url! as NSURL) let provider = NSItemProvider(object: status.url! as NSURL)
let activity = UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID) let activity = UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID)
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all) provider.registerObject(activity, visibility: .all)
return [UIDragItem(itemProvider: provider)] return [UIDragItem(itemProvider: provider)]
} }