UI iOS 13 中场景的状态恢复,同时仍然支持 iOS 12。没有故事板

UI state restoration for a scene in iOS 13 while still supporting iOS 12. No storyboards

这有点长,但不是微不足道的,需要很多时间来证明这个问题。

我正在尝试弄清楚如何将一个小示例应用程序从 iOS 12 更新到 iOS 13。此示例应用程序不使用任何故事板(启动屏幕除外) .这是一个简单的应用程序,它显示一个带有由计时器更新的标签的视图控制器。它使用状态恢复,因此计数器从它停止的地方开始。我希望能够支持iOS12和iOS13。在iOS13我想更新到新的场景架构。

在 iOS 12 下,该应用程序运行良好。在全新安装中,计数器从 0 开始上升。将应用程序置于后台,然后重新启动应用程序,计数器从停止的地方继续。状态恢复一切正常。

现在我正在尝试使用场景在 iOS 13 下工作。我遇到的问题是找出初始化场景 window 并将导航控制器和主视图控制器恢复到场景的正确方法。

我已经阅读了尽可能多的与状态恢复和场景相关的 Apple 文档。我看过与 windows 和场景 (212 - Introducing Multiple Windows on iPad, 258 - Architecting Your App for Multiple Windows) 相关的 WWDC 视频。但是我好像少了一块拼起来的。

当我 运行 iOS 13 下的应用程序时,所有预期的委托方法(AppDelegate 和 SceneDelegate)都被调用。状态恢复正在恢复导航控制器和主视图控制器,但我不知道如何设置场景 window 的 rootViewController,因为所有 UI 状态恢复都在AppDelegate.

似乎还有一些与应该使用的 NSUserTask 有关的内容,但我无法将这些点联系起来。

缺失的部分似乎在SceneDelegatewillConnectTo方法中。我确定我还需要对 SceneDelegatestateRestorationActivity 进行一些更改。 AppDelegate 中可能还需要更改。我怀疑 ViewController 中的任何内容都需要更改。


要复制我正在做的事情,请使用单视图应用程序模板创建一个新的 iOS 项目 Xcode 11(目前为 beta 4)。将部署目标设置为 iOS 11 或 12。

删除主故事板。删除 Info.plist 中对 Main 的两个引用(一个在顶层,一个在 Application Scene Manifest 的深处。更新 3 个 swift 文件如下。

AppDelegate.swift:

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        print("AppDelegate willFinishLaunchingWithOptions")

        // This probably shouldn't be run under iOS 13?
        self.window = UIWindow(frame: UIScreen.main.bounds)

        return true
    }

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        print("AppDelegate didFinishLaunchingWithOptions")

        if #available(iOS 13.0, *) {
            // What needs to be here?
        } else {
            // If the root view controller wasn't restored, create a new one from scratch
            if (self.window?.rootViewController == nil) {
                let vc = ViewController()
                let nc = UINavigationController(rootViewController: vc)
                nc.restorationIdentifier = "RootNC"

                self.window?.rootViewController = nc
            }

            self.window?.makeKeyAndVisible()
        }

        return true
    }

    func application(_ application: UIApplication, viewControllerWithRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
        print("AppDelegate viewControllerWithRestorationIdentifierPath")

        // If this is for the nav controller, restore it and set it as the window's root
        if identifierComponents.first == "RootNC" {
            let nc = UINavigationController()
            nc.restorationIdentifier = "RootNC"
            self.window?.rootViewController = nc

            return nc
        }

        return nil
    }

    func application(_ application: UIApplication, willEncodeRestorableStateWith coder: NSCoder) {
        print("AppDelegate willEncodeRestorableStateWith")

        // Trigger saving of the root view controller
        coder.encode(self.window?.rootViewController, forKey: "root")
    }

    func application(_ application: UIApplication, didDecodeRestorableStateWith coder: NSCoder) {
        print("AppDelegate didDecodeRestorableStateWith")
    }

    func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
        print("AppDelegate shouldSaveApplicationState")

        return true
    }

    func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
        print("AppDelegate shouldRestoreApplicationState")

        return true
    }

    // The following four are not called in iOS 13
    func applicationWillEnterForeground(_ application: UIApplication) {
        print("AppDelegate applicationWillEnterForeground")
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        print("AppDelegate applicationDidEnterBackground")
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        print("AppDelegate applicationDidBecomeActive")
    }

    func applicationWillResignActive(_ application: UIApplication) {
        print("AppDelegate applicationWillResignActive")
    }

    // MARK: UISceneSession Lifecycle

    @available(iOS 13.0, *)
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        print("AppDelegate configurationForConnecting")

        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    @available(iOS 13.0, *)
    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        print("AppDelegate didDiscardSceneSessions")
    }
}

SceneDelegate.swift:

import UIKit

@available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        print("SceneDelegate willConnectTo")

        guard let winScene = (scene as? UIWindowScene) else { return }

        // Got some of this from WWDC2109 video 258
        window = UIWindow(windowScene: winScene)
        if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
            // Now what? How to connect the UI restored in the AppDelegate to this window?
        } else {
            // Create the initial UI if there is nothing to restore
            let vc = ViewController()
            let nc = UINavigationController(rootViewController: vc)
            nc.restorationIdentifier = "RootNC"

            self.window?.rootViewController = nc
            window?.makeKeyAndVisible()
        }
    }

    func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
        print("SceneDelegate stateRestorationActivity")

        // What should be done here?
        let activity = NSUserActivity(activityType: "What?")
        activity.persistentIdentifier = "huh?"

        return activity
    }

    func scene(_ scene: UIScene, didUpdate userActivity: NSUserActivity) {
        print("SceneDelegate didUpdate")
    }

    func sceneDidDisconnect(_ scene: UIScene) {
        print("SceneDelegate sceneDidDisconnect")
    }

    func sceneDidBecomeActive(_ scene: UIScene) {
        print("SceneDelegate sceneDidBecomeActive")
    }

    func sceneWillResignActive(_ scene: UIScene) {
        print("SceneDelegate sceneWillResignActive")
    }

    func sceneWillEnterForeground(_ scene: UIScene) {
        print("SceneDelegate sceneWillEnterForeground")
    }

    func sceneDidEnterBackground(_ scene: UIScene) {
        print("SceneDelegate sceneDidEnterBackground")
    }
}

ViewController.swift:

import UIKit

class ViewController: UIViewController, UIViewControllerRestoration {
    var label: UILabel!
    var count: Int = 0
    var timer: Timer?

    static func viewController(withRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
        print("ViewController withRestorationIdentifierPath")

        return ViewController()
    }

    override init(nibName nibNameOrNil: String? = nil, bundle nibBundleOrNil: Bundle? = nil) {
        print("ViewController init")

        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)

        restorationIdentifier = "ViewController"
        restorationClass = ViewController.self
    }

    required init?(coder: NSCoder) {
        print("ViewController init(coder)")

        super.init(coder: coder)
    }

    override func viewDidLoad() {
        print("ViewController viewDidLoad")

        super.viewDidLoad()

        view.backgroundColor = .green // be sure this vc is visible

        label = UILabel(frame: .zero)
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "\(count)"
        view.addSubview(label)
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])
    }

    override func viewWillAppear(_ animated: Bool) {
        print("ViewController viewWillAppear")

        super.viewWillAppear(animated)

        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { (timer) in
            self.count += 1
            self.label.text = "\(self.count)"
        })
    }

    override func viewDidDisappear(_ animated: Bool) {
        print("ViewController viewDidDisappear")

        super.viewDidDisappear(animated)

        timer?.invalidate()
        timer = nil
    }

    override func encodeRestorableState(with coder: NSCoder) {
        print("ViewController encodeRestorableState")

        super.encodeRestorableState(with: coder)

        coder.encode(count, forKey: "count")
    }

    override func decodeRestorableState(with coder: NSCoder) {
        print("ViewController decodeRestorableState")

        super.decodeRestorableState(with: coder)

        count = coder.decodeInteger(forKey: "count")
        label.text = "\(count)"
    }
}

运行 在 iOS 11 或 12 下,它工作正常。

您可以在 iOS 下 运行 13 并在全新安装应用程序时获得 UI。但是应用程序的任何后续 运行 都会出现黑屏,因为通过状态恢复恢复的 UI 没有连接到场景的 window.

我错过了什么?这只是缺少一两行代码还是我对 iOS 13 场景状态恢复的整个方法是错误的?

请记住,一旦我明白了这一点,下一步将支持多个 windows。所以该解决方案应该适用于多个场景,而不仅仅是一个。

为了支持 iOS 13 中的状态恢复,您需要将足够的状态编码到 NSUserActivity:

Use this method to return an NSUserActivity object with information about your scene's data. Save enough information to be able to retrieve that data again after UIKit disconnects and then reconnects the scene. User activity objects are meant for recording what the user was doing, so you don't need to save the state of your scene's UI

这种方法的优点是可以更轻松地支持切换,因为您正在创建必要的代码来通过用户活动保持和恢复状态。

与之前 iOS 为您重新创建视图控制器层次结构的状态恢复方法不同,您负责在场景委托中为您的场景创建视图层次结构。

如果您有多个活动场景,那么您的委托将被调用多次以保存状态并多次调用以恢复状态;不需要什么特别的。

我对您的代码所做的更改是:

AppDelegate.swift

在 iOS 13 及更高版本上禁用 "legacy" 状态恢复:

func application(_ application: UIApplication, viewControllerWithRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
    if #available(iOS 13, *) {

    } else {
        print("AppDelegate viewControllerWithRestorationIdentifierPath")

        // If this is for the nav controller, restore it and set it as the window's root
        if identifierComponents.first == "RootNC" {
            let nc = UINavigationController()
            nc.restorationIdentifier = "RootNC"
            self.window?.rootViewController = nc

            return nc
        }
    }
    return nil
}

func application(_ application: UIApplication, willEncodeRestorableStateWith coder: NSCoder) {
    print("AppDelegate willEncodeRestorableStateWith")
    if #available(iOS 13, *) {

    } else {
    // Trigger saving of the root view controller
        coder.encode(self.window?.rootViewController, forKey: "root")
    }
}

func application(_ application: UIApplication, didDecodeRestorableStateWith coder: NSCoder) {
    print("AppDelegate didDecodeRestorableStateWith")
}

func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
    print("AppDelegate shouldSaveApplicationState")
    if #available(iOS 13, *) {
        return false
    } else {
        return true
    }
}

func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
    print("AppDelegate shouldRestoreApplicationState")
    if #available(iOS 13, *) {
        return false
    } else {
        return true
    }
}

SceneDelegate.swift

在需要时创建用户 activity 并使用它重新创建视图控制器。请注意,您负责在正常和恢复情况下创建视图层次结构。

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    print("SceneDelegate willConnectTo")

    guard let winScene = (scene as? UIWindowScene) else { return }

    // Got some of this from WWDC2109 video 258
    window = UIWindow(windowScene: winScene)

    let vc = ViewController()

    if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
        vc.continueFrom(activity: activity)
    }

    let nc = UINavigationController(rootViewController: vc)
    nc.restorationIdentifier = "RootNC"

    self.window?.rootViewController = nc
    window?.makeKeyAndVisible()


}

func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
    print("SceneDelegate stateRestorationActivity")

    if let nc = self.window?.rootViewController as? UINavigationController, let vc = nc.viewControllers.first as? ViewController {
        return vc.continuationActivity
    } else {
        return nil
    }

}

ViewController.swift

添加对从 NSUserActivity.

保存和加载的支持
var continuationActivity: NSUserActivity {
    let activity = NSUserActivity(activityType: "restoration")
    activity.persistentIdentifier = UUID().uuidString
    activity.addUserInfoEntries(from: ["Count":self.count])
    return activity
}

func continueFrom(activity: NSUserActivity) {
    let count = activity.userInfo?["Count"] as? Int ?? 0
    self.count = count
}

基于更多研究和来自 的非常有用的建议,我想出了一种适用于 iOS 13 和 iOS 12(及更早版本)的方法,并且不会重复代码并对 iOS.

的所有版本使用相同的方法

请注意,虽然原始问题和此答案不使用故事板,但解决方案本质上是相同的。唯一的区别是,对于故事板,AppDelegate 和 SceneDelegate 不需要代码来创建 window 和根视图控制器。当然 ViewController 不需要代码来创建它的视图。

基本思路是将iOS 12 代码迁移到与iOS 13 相同的工作方式。这意味着不再使用旧状态恢复。 NSUserTask 用于保存和恢复状态。这种方法有几个好处。它让相同的代码适用于所有 iOS 版本,它让你几乎不需要额外的努力就可以真正接近支持切换,它让你支持多个 window 场景和使用相同的基本代码的完整状态恢复.

这是更新后的 AppDelegate.swift:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        print("AppDelegate willFinishLaunchingWithOptions")

        if #available(iOS 13.0, *) {
            // no-op - UI created in scene delegate
        } else {
            self.window = UIWindow(frame: UIScreen.main.bounds)
            let vc = ViewController()
            let nc = UINavigationController(rootViewController: vc)

            self.window?.rootViewController = nc

            self.window?.makeKeyAndVisible()
        }

        return true
    }

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        print("AppDelegate didFinishLaunchingWithOptions")

        return true
    }

    func application(_ application: UIApplication, viewControllerWithRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
        print("AppDelegate viewControllerWithRestorationIdentifierPath")

        return nil // We don't want any UI hierarchy saved
    }

    func application(_ application: UIApplication, willEncodeRestorableStateWith coder: NSCoder) {
        print("AppDelegate willEncodeRestorableStateWith")

        if #available(iOS 13.0, *) {
            // no-op
        } else {
            // This is the important link for iOS 12 and earlier
            // If some view in your app sets a user activity on its window,
            // here we give the view hierarchy a chance to update the user
            // activity with whatever state info it needs to record so it can
            // later be restored to restore the app to its previous state.
            if let activity = window?.userActivity {
                activity.userInfo = [:]
                ((window?.rootViewController as? UINavigationController)?.viewControllers.first as? ViewController)?.updateUserActivityState(activity)

                // Now save off the updated user activity
                let wrap = NSUserActivityWrapper(activity)
                coder.encode(wrap, forKey: "userActivity")
            }
        }
    }

    func application(_ application: UIApplication, didDecodeRestorableStateWith coder: NSCoder) {
        print("AppDelegate didDecodeRestorableStateWith")

        // If we find a stored user activity, load it and give it to the view
        // hierarchy so the UI can be restored to its previous state
        if let wrap = coder.decodeObject(forKey: "userActivity") as? NSUserActivityWrapper {
            ((window?.rootViewController as? UINavigationController)?.viewControllers.first as? ViewController)?.restoreUserActivityState(wrap.userActivity)
        }
    }

    func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
        print("AppDelegate shouldSaveApplicationState")

        if #available(iOS 13.0, *) {
            return false
        } else {
            // Enabled just so we can persist the NSUserActivity if there is one
            return true
        }
    }

    func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
        print("AppDelegate shouldRestoreApplicationState")

        if #available(iOS 13.0, *) {
            return false
        } else {
            return true
        }
    }

    // MARK: UISceneSession Lifecycle

    @available(iOS 13.0, *)
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        print("AppDelegate configurationForConnecting")

        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    @available(iOS 13.0, *)
    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        print("AppDelegate didDiscardSceneSessions")
    }
}

在 iOS 12 及更早版本中,标准状态恢复过程现在仅用于 save/restore NSUserActivity。它不再用于保留视图层次结构。

由于 NSUserActivity 不符合 NSCoding,因此使用了包装器 class。

NSUserActivityWrapper.swift:

import Foundation

class NSUserActivityWrapper: NSObject, NSCoding {
    private (set) var userActivity: NSUserActivity

    init(_ userActivity: NSUserActivity) {
        self.userActivity = userActivity
    }

    required init?(coder: NSCoder) {
        if let activityType = coder.decodeObject(forKey: "activityType") as? String {
            userActivity = NSUserActivity(activityType: activityType)
            userActivity.title = coder.decodeObject(forKey: "activityTitle") as? String
            userActivity.userInfo = coder.decodeObject(forKey: "activityUserInfo") as? [AnyHashable: Any]
        } else {
            return nil;
        }
    }

    func encode(with coder: NSCoder) {
        coder.encode(userActivity.activityType, forKey: "activityType")
        coder.encode(userActivity.title, forKey: "activityTitle")
        coder.encode(userActivity.userInfo, forKey: "activityUserInfo")
    }
}

请注意,根据您的需要,可能需要 NSUserActivity 的其他属性。

这是更新后的 SceneDelegate.swift:

import UIKit

@available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        print("SceneDelegate willConnectTo")

        guard let winScene = (scene as? UIWindowScene) else { return }

        window = UIWindow(windowScene: winScene)

        let vc = ViewController()
        let nc = UINavigationController(rootViewController: vc)

        if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
            vc.restoreUserActivityState(activity)
        }

        self.window?.rootViewController = nc
        window?.makeKeyAndVisible()
    }

    func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
        print("SceneDelegate stateRestorationActivity")

        if let activity = window?.userActivity {
            activity.userInfo = [:]
            ((window?.rootViewController as? UINavigationController)?.viewControllers.first as? ViewController)?.updateUserActivityState(activity)

            return activity
        }

        return nil
    }
}

最后更新 ViewController.swift:

import UIKit

class ViewController: UIViewController {
    var label: UILabel!
    var count: Int = 0 {
        didSet {
            if let label = self.label {
                label.text = "\(count)"
            }
        }
    }
    var timer: Timer?

    override func viewDidLoad() {
        print("ViewController viewDidLoad")

        super.viewDidLoad()

        view.backgroundColor = .green

        label = UILabel(frame: .zero)
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "\(count)"
        view.addSubview(label)
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])
    }

    override func viewWillAppear(_ animated: Bool) {
        print("ViewController viewWillAppear")

        super.viewWillAppear(animated)

        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { (timer) in
            self.count += 1
            //self.userActivity?.needsSave = true
        })
        self.label.text = "\(count)"
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        let act = NSUserActivity(activityType: "com.whatever.View")
        act.title = "View"
        self.view.window?.userActivity = act
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        self.view.window?.userActivity = nil
    }

    override func viewDidDisappear(_ animated: Bool) {
        print("ViewController viewDidDisappear")

        super.viewDidDisappear(animated)

        timer?.invalidate()
        timer = nil
    }

    override func updateUserActivityState(_ activity: NSUserActivity) {
        print("ViewController updateUserActivityState")
        super.updateUserActivityState(activity)

        activity.addUserInfoEntries(from: ["count": count])
    }

    override func restoreUserActivityState(_ activity: NSUserActivity) {
        print("ViewController restoreUserActivityState")
        super.restoreUserActivityState(activity)

        count = activity.userInfo?["count"] as? Int ?? 0
    }
}

请注意,所有与旧状态恢复相关的代码都已删除。它已被 NSUserActivity.

的使用所取代

在真实的应用程序中,您会在用户中存储各种其他详细信息 activity 以在重新启动时完全恢复应用程序状态或支持切换。或者存储启动新 window 场景所需的最少数据。

您还希望在真实应用中根据需要将对 updateUserActivityStaterestoreUserActivityState 的调用链接到任何子视图。

在我看来,这是 结构中的主要缺陷:

You would also want to chain calls to updateUserActivityState

这错过了 updateUserActivityState 的全部要点,即 自动为您调用 ,对于 userActivityuserActivity 的所有视图控制器与场景委托的 stateRestorationActivity.

返回的 NSUserActivity 相同

因此,我们自动有了状态保存机制,剩下的就是设计一个状态恢复机制来匹配。我将说明我提出的整个架构。

注意: 这个讨论忽略了多个 windows 并且它也忽略了问题的原始要求,即我们与 iOS 12 视图控制器兼容基于状态的保存和恢复。我在这里的目标只是展示 如何使用 NSUserActivity 在 iOS 13 中进行状态保存和恢复。 然而,只需要稍作修改就可以将其折叠成多个- window 应用程序,所以我认为它充分回答了原始问题。

节省

让我们从状态保存开始。这完全是样板。场景委托创建场景 userActivity 或将接收到的恢复 activity 传递给它,并且 returns 作为它自己的用户 activity:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    guard let scene = (scene as? UIWindowScene) else { return }
    scene.userActivity =
        session.stateRestorationActivity ??
        NSUserActivity(activityType: "restoration")
}
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
    return scene.userActivity
}

每个视图控制器必须使用自己的 viewDidAppear 共享 用户 activity 对象。这样,当我们进入后台时,它自己的 updateUserActivityState 将被自动调用 ,并且它有机会贡献给用户信息的全局池:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    self.userActivity = self.view.window?.windowScene?.userActivity
}
// called automatically at saving time!
override func updateUserActivityState(_ activity: NSUserActivity) {
    super.updateUserActivityState(activity)
    // gather info into `info`
    activity.addUserInfoEntries(from: info)
}

就是这样!如果每个视图控制器都这样做,那么在我们进入后台时每个处于活动状态的视图控制器都有机会为下次启动时到达的用户 activity 的用户信息做出贡献。

修复

这部分比较难。恢复信息将作为 session.stateRestorationActivity 到达场景委托。正如最初的问题正确地问:现在怎么办?

给这只猫剥皮的方法不止一种,我已经尝试了其中的大部分,最后选择了这个。我的规则是:

  • 每个视图控制器都必须有一个 restorationInfo 属性 这是一个字典。在恢复期间创建任何视图控制器时,其创建者(父级)必须将 restorationInfo 设置为来自 session.stateRestorationActivity.

  • userInfo
  • 这个userInfo必须在一开始就复制出来,因为它会在第一次调用updateUserActivityState时从保存的activity中删除(这是真正让我疯狂地研究这个架构的部分。

很酷的部分是,如果我们做对了,restorationInfo 设置在 之前 viewDidLoad,因此视图控制器可以根据关于保存到字典中的信息。

每个视图控制器在用完后还必须删除它自己的restorationInfo,以免在应用程序的生命周期内再次使用它。它只能在启动时使用一次。

所以我们必须改变我们的样板:

var restorationInfo :  [AnyHashable : Any]?
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    self.userActivity = self.view.window?.windowScene?.userActivity
    self.restorationInfo = nil
}

所以现在唯一的问题是如何设置每个视图控制器的 restorationInfo 链。链从场景委托开始,它负责在根视图控制器中设置这个属性:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    guard let scene = (scene as? UIWindowScene) else { return }
    scene.userActivity =
        session.stateRestorationActivity ??
        NSUserActivity(activityType: "restoration")
    if let rvc = window?.rootViewController as? RootViewController {
        rvc.restorationInfo = scene.userActivity?.userInfo
    }
}

然后每个视图控制器不仅负责根据 restorationInfo 在其 viewDidLoad 中配置自己,还负责查看它是否是任何其他视图控制器的父级/呈现器.如果是这样,它必须创建并呈现/推送/任何该视图控制器,确保在该子视图控制器的 viewDidLoad 运行之前传递 restorationInfo

如果每个视图控制器都正确执行此操作,则整个界面和状态将恢复!

多一点例子

假设我们只有两个可能的视图控制器:RootViewController 和 PresentedViewController。要么 RootViewController 在我们进入后台时呈现 PresentedViewController ,要么不是。无论哪种方式,该信息都已写入信息字典。

RootViewController 的作用如下:

var restorationInfo : [AnyHashable:Any]?
override func viewDidLoad() {
    super.viewDidLoad()
    // configure self, including any info from restoration info
}

// this is the earliest we have a window, so it's the earliest we can present
// if we are restoring the editing window
var didFirstWillLayout = false
override func viewWillLayoutSubviews() {
    if didFirstWillLayout { return }
    didFirstWillLayout = true
    let key = PresentedViewController.editingRestorationKey
    let info = self.restorationInfo
    if let editing = info?[key] as? Bool, editing {
        self.performSegue(withIdentifier: "PresentWithNoAnimation", sender: self)
    }
}

// boilerplate
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    self.userActivity = self.view.window?.windowScene?.userActivity
    self.restorationInfo = nil
}

// called automatically because we share this activity with the scene
override func updateUserActivityState(_ activity: NSUserActivity) {
    super.updateUserActivityState(activity)
    // express state as info dictionary
    activity.addUserInfoEntries(from: info)
}

最酷的部分是 PresentedViewController 做的事情完全一样!

var restorationInfo :  [AnyHashable : Any]?
static let editingRestorationKey = "editing"

override func viewDidLoad() {
    super.viewDidLoad()
    // configure self, including info from restoration info
}

// boilerplate
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    self.userActivity = self.view.window?.windowScene?.userActivity
    self.restorationInfo = nil
}

override func updateUserActivityState(_ activity: NSUserActivity) {
    super.updateUserActivityState(activity)
    let key = Self.editingRestorationKey
    activity.addUserInfoEntries(from: [key:true])
    // and add any other state info as well
}

我想你可以看到,在这一点上,这只是一个程度的问题。如果我们在恢复过程中有更多的视图控制器要链接,它们的工作方式完全相同。

最后的笔记

正如我所说,这不是给修复猫剥皮的唯一方法。但是存在时间和责任分配的问题,我认为这是最公平的做法。

特别是,我不赞成场景委托负责整个界面的还原。它需要了解太多关于如何沿线初始化每个视图控制器的细节,并且存在严重的时序问题,很难以确定性的方式克服。我的方法有点模仿旧的基于视图控制器的恢复,使每个视图控制器以与通常相同的方式负责其子视图。

Apple 于 2019 年 9 月 6 日发布了 this sample app,展示了 iOS 13 状态恢复并向后兼容 iOS 12。

来自Readme.md

示例支持两种不同的状态保存方法。在 iOS 13 及更高版本中,应用程序使用 NSUserActivity 对象为每个 window 场景保存状态。在 iOS 12 及更早版本中,应用程序通过保存和恢复视图控制器的配置来保留其用户界面的状态。

自述文件详细介绍了它的工作原理 - 基本技巧是在 iOS 12 上它对 Activity 对象(在 iOS 12 中可用于其他目的)进行编码旧 encodeRestorableState 方法。

override func encodeRestorableState(with coder: NSCoder) {
    super.encodeRestorableState(with: coder)

    let encodedActivity = NSUserActivityEncoder(detailUserActivity)
    coder.encode(encodedActivity, forKey: DetailViewController.restoreActivityKey)
}

并且在 iOS 13 它使用 SceneDelegate.

的配置方法实现了丢失的自动视图控制器层次结构恢复
func configure(window: UIWindow?, with activity: NSUserActivity) -> Bool {
    if let detailViewController = DetailViewController.loadFromStoryboard() {
        if let navigationController = window?.rootViewController as? UINavigationController {
            navigationController.pushViewController(detailViewController, animated: false)
            detailViewController.restoreUserActivityState(activity)
            return true
        }
    }
    return false
}

最后,自述文件包含测试建议,但如果您先启动 Xcode 10.2 模拟器,我想补充一下,例如iPhone 8 Plus,然后启动 Xcode 11,您将可以选择 iPhone 8 Plus (12.4),并且可以体验向后兼容的行为。我也喜欢使用这些用户默认值,第二个允许恢复存档在崩溃中幸存下来:

[NSUserDefaults.standardUserDefaults setBool:YES forKey:@"UIStateRestorationDebugLogging"];
[NSUserDefaults.standardUserDefaults setBool:YES forKey:@"UIStateRestorationDeveloperMode"];