From 4a9e79cd1e0ce007fe5c1fc5a6c492115df9ccf3 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 26 Nov 2019 16:33:11 -0600 Subject: [PATCH] Persist and restore container expanded state across application launches. Issue #1361 --- Frameworks/Account/ContainerIdentifier.swift | 38 +++++++++++++++++++ Shared/Activity/ActivityManager.swift | 16 ++++++-- Shared/Activity/ActivityType.swift | 1 + Shared/UserInfoKey.swift | 3 ++ iOS/MasterFeed/MasterFeedViewController.swift | 5 ++- iOS/Resources/Info.plist | 1 + iOS/SceneCoordinator.swift | 34 +++++++++++++++-- iOS/SceneDelegate.swift | 4 ++ 8 files changed, 92 insertions(+), 10 deletions(-) diff --git a/Frameworks/Account/ContainerIdentifier.swift b/Frameworks/Account/ContainerIdentifier.swift index 9745a80db..47d87e4b6 100644 --- a/Frameworks/Account/ContainerIdentifier.swift +++ b/Frameworks/Account/ContainerIdentifier.swift @@ -16,4 +16,42 @@ public enum ContainerIdentifier: Hashable { case smartFeedController case account(String) // accountID case folder(String, String) // accountID, folderName + + public var userInfo: [AnyHashable: Any] { + switch self { + case .smartFeedController: + return [ + "type": "smartFeedController" + ] + case .account(let accountID): + return [ + "type": "account", + "accountID": accountID + ] + case .folder(let accountID, let folderName): + return [ + "type": "folder", + "accountID": accountID, + "folderName": folderName + ] + } + } + + public init?(userInfo: [AnyHashable: Any]) { + guard let type = userInfo["type"] as? String else { return nil } + + switch type { + case "smartFeedController": + self = ContainerIdentifier.smartFeedController + case "account": + guard let accountID = userInfo["accountID"] as? String else { return nil } + self = ContainerIdentifier.account(accountID) + case "folder": + guard let accountID = userInfo["accountID"] as? String, let folderName = userInfo["folderName"] as? String else { return nil } + self = ContainerIdentifier.folder(accountID, folderName) + default: + return nil + } + } + } diff --git a/Shared/Activity/ActivityManager.swift b/Shared/Activity/ActivityManager.swift index 25403da8f..ef26b958a 100644 --- a/Shared/Activity/ActivityManager.swift +++ b/Shared/Activity/ActivityManager.swift @@ -21,11 +21,19 @@ class ActivityManager { private var readingActivity: NSUserActivity? private var readingArticle: Article? - var stateRestorationActivity: NSUserActivity? { - if readingActivity != nil { - return readingActivity + var stateRestorationActivity: NSUserActivity { + if let activity = readingActivity { + return activity } - return selectingActivity + + if let activity = selectingActivity { + return activity + } + + let activity = NSUserActivity(activityType: ActivityType.restoration.rawValue) + activity.persistentIdentifier = UUID().uuidString + activity.becomeCurrent() + return activity } init() { diff --git a/Shared/Activity/ActivityType.swift b/Shared/Activity/ActivityType.swift index 921bccda5..53d8fe9d3 100644 --- a/Shared/Activity/ActivityType.swift +++ b/Shared/Activity/ActivityType.swift @@ -9,6 +9,7 @@ import Foundation enum ActivityType: String { + case restoration = "Restoration" case selectFeed = "SelectFeed" case nextUnread = "NextUnread" case readArticle = "ReadArticle" diff --git a/Shared/UserInfoKey.swift b/Shared/UserInfoKey.swift index 1318b0ea0..81f939766 100644 --- a/Shared/UserInfoKey.swift +++ b/Shared/UserInfoKey.swift @@ -23,4 +23,7 @@ struct UserInfoKey { static let articlePath = "articlePath" static let feedIdentifier = "feedIdentifier" + static let windowState = "windowState" + static let containerExpandedWindowState = "containerExpandedWindowState" + } diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 67dbbb1e0..df391180f 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -540,8 +540,9 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } if let indexPath = dataSource.indexPath(for: node) { - coordinator.selectFeed(indexPath, animated: animated) - completion?() + coordinator.selectFeed(indexPath, animated: animated) { + completion?() + } return } diff --git a/iOS/Resources/Info.plist b/iOS/Resources/Info.plist index fa66f10ec..a03e1e166 100644 --- a/iOS/Resources/Info.plist +++ b/iOS/Resources/Info.plist @@ -60,6 +60,7 @@ Grant permission to save images from the article. NSUserActivityTypes + Restoration AddWebFeedIntent NextUnread ReadArticle diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 3463ebe9a..1ca436875 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -101,8 +101,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { private let treeControllerDelegate = WebFeedTreeControllerDelegate() private let treeController: TreeController - var stateRestorationActivity: NSUserActivity? { - return activityManager.stateRestorationActivity + var stateRestorationActivity: NSUserActivity { + let activity = activityManager.stateRestorationActivity + var userInfo = activity.userInfo == nil ? [AnyHashable: Any]() : activity.userInfo + userInfo![UserInfoKey.windowState] = windowState() + activity.userInfo = userInfo + return activity } var isRootSplitCollapsed: Bool { @@ -315,11 +319,20 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return rootSplitViewController } + func restoreWindowState(_ activity: NSUserActivity) { + if let windowState = activity.userInfo?[UserInfoKey.windowState] as? [AnyHashable: Any] { + restoreWindowState(windowState) + rebuildShadowTable() + masterFeedViewController.reloadFeeds() + } + } + func handle(_ activity: NSUserActivity) { selectFeed(nil, animated: false) { - guard let activityType = ActivityType(rawValue: activity.activityType) else { return } switch activityType { + case .restoration: + break case .selectFeed: self.handleSelectFeed(activity.userInfo) case .nextUnread: @@ -329,7 +342,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { case .addFeedIntent: self.showAdd(.feed) } - } } @@ -1744,6 +1756,20 @@ private extension SceneCoordinator { // MARK: NSUserActivity + func windowState() -> [AnyHashable: Any] { + let containerIdentifierUserInfos = expandedTable.map( { $0.userInfo }) + return [ + UserInfoKey.containerExpandedWindowState: containerIdentifierUserInfos + ] + } + + func restoreWindowState(_ windowState: [AnyHashable: Any]) { + if let containerIdentifierUserInfos = windowState[UserInfoKey.containerExpandedWindowState] as? [[AnyHashable: Any]] { + let containerIdentifers = containerIdentifierUserInfos.compactMap( { ContainerIdentifier(userInfo: $0) }) + expandedTable = Set(containerIdentifers) + } + } + func handleSelectFeed(_ userInfo: [AnyHashable : Any]?) { guard let userInfo = userInfo, let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : Any], diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index 0a8868d1a..839517f07 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -23,6 +23,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window!.tintColor = AppAssets.primaryAccentColor window!.rootViewController = coordinator.start(for: window!.frame.size) + if let stateRestorationActivity = session.stateRestorationActivity { + coordinator.restoreWindowState(stateRestorationActivity) + } + if let shortcutItem = connectionOptions.shortcutItem { window!.makeKeyAndVisible() handleShortcutItem(shortcutItem)