25

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

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

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

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

我已经阅读了尽可能多的与状态恢复和场景相关的 Apple 文档。我看过与窗口和场景相关的 WWDC 视频(212 - 在 iPad 上介绍多个 Windows258 - 为多个 Windows 构建您的应用程序)。但我似乎错过了将它们放在一起的部分。

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

似乎还有一些与NSUserTask应该使用的相关的东西,但我无法连接这些点。

缺少的部分似乎在willConnectTo方法中SceneDelegate。我确定我还需要stateRestorationActivitySceneDelegate. 可能还需要更改AppDelegate. 我怀疑有什么ViewController需要改变的。


要复制我正在做的事情,请使用 Single View App 模板使用 Xcode 11(目前为 beta 4)创建一个新的 iOS 项目。将部署目标设置为 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 未连接到场景的窗口。

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

请记住,一旦我弄清楚了下一步将支持多个窗口。因此,该解决方案应该适用于多个场景,而不仅仅是一个场景。

4

4 回答 4

18

在我看来,这是迄今为止提出的答案结构中的主要缺陷:

您还想将调用链接到updateUserActivityState

这错过了 的全部要点updateUserActivityState,即它会自动为您调用所有与场景委托返回的 NSUserActivity相同userActivity的视图控制器。stateRestorationActivity

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

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

保存

让我们从状态保存开始。这完全是样板文件。场景委托要么创建场景userActivity,要么将接收到的恢复活动传递给它,并将其作为自己的用户活动返回:

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共享该用户活动对象。这样,当我们进入后台时,它自己的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)
}

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

恢复

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

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

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

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

很酷的部分是,如果我们正确地执行此操作,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
    }
}

然后,每个视图控制器不仅负责viewDidLoad基于restorationInfo. 如果是这样,它必须创建并呈现/推送/无论该视图控制器,确保restorationInfo在该子视图控制器viewDidLoad运行之前传递。

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

再举个例子

假设我们只有两个可能的视图控制器: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
}

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

最后的笔记

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

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

于 2019-09-10T22:21:28.980 回答
17

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

使用此方法返回一个 NSUserActivity 对象,其中包含有关场景数据的信息。保存足够的信息,以便在 UIKit 断开连接然后重新连接场景后再次检索该数据。用户活动对象用于记录用户所做的事情,因此您无需保存场景 UI 的状态

这种方法的优点是它可以更容易地支持切换,因为您正在创建通过用户活动保持和恢复状态所需的代码。

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

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

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

AppDelegate.swift

在 iOS 13 及更高版本上禁用“旧版”状态恢复:

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

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

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
}
于 2019-07-21T09:40:49.160 回答
9

根据Paulw11 的答案的更多研究和非常有用的建议,我提出了一种适用于 iOS 13 和 iOS 12(及更早版本)的方法,无需重复代码,并对所有版本的 iOS 使用相同的方法。

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

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

这是更新后的 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 及更早版本下,标准状态恢复过程现在仅用于保存/恢复NSUserActivity. 它不再用于持久化视图层次结构。

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

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.

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

您还希望在实际应用程序中根据需要链接updateUserActivityStaterestoreUserActivityState任何子视图的调用。

于 2019-07-25T23:09:22.113 回答
6

2019 年 9 月 6 日,Apple 发布了此示例应用程序,该应用程序演示了 iOS 13 状态恢复以及与 iOS 12 的向后兼容性。

来自 Readme.md

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

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

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"];
于 2019-09-16T10:48:02.647 回答