在iOS 13中场景的UI状态恢复,同时仍支持iOS 12。

rma*_*ddy 11 ios state-restoration ios13 uiscene

这有点长,但并不琐碎,需要花很多时间来证明这个问题。

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

在iOS 12下,该应用程序可以正常运行。在全新安装中,计数器从0开始并上升。将应用程序置于后台,然后重新启动应用程序,然后计数器从停止处继续计数。状态恢复全部正常。

现在,我正在尝试使用场景在iOS 13下运行该功能。我遇到的问题是找出初始化场景窗口并将导航控制器和主视图控制器还原到场景的正确方法。

我已经阅读了尽可能多的Apple文档,这些文档与状态恢复和场景有关。我看过与窗口和场景相关的WWDC视频(212-在iPad上引入多个Windows,258-为多个Windows架构应用)。但我似乎缺少将所有内容组合在一起的作品。

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

似乎也有一些与NSUserTask应该使用的相关的东西,但我无法说明问题。

缺少的部分似乎是在willConnectTo方法中SceneDelegate。我敢肯定,我还需要一些变化stateRestorationActivity的SceneDelegate。中可能还需要进行更改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")
    }
}
Run Code Online (Sandbox Code Playgroud)

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")
    }
}
Run Code Online (Sandbox Code Playgroud)

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)"
    }
}
Run Code Online (Sandbox Code Playgroud)

在iOS 11或12下运行它,效果很好。

您可以在iOS 13下运行此程序,并在全新安装的应用程序上获得UI。但是此应用程序的任何后续运行都会显示黑屏,因为通过状态还原还原的UI未连接到场景的窗口。

我想念什么?这是只是缺少一两行代码还是我对iOS 13场景状态还原的整个方法是否错误?

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

Pau*_*w11 11

为了支持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
    }
}
Run Code Online (Sandbox Code Playgroud)

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
    }

}
Run Code Online (Sandbox Code Playgroud)

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
}
Run Code Online (Sandbox Code Playgroud)

  • 当我在发布后进一步研究这个问题时,我开始意识到 iOS 13 中的状态恢复更像是切换功能。遗憾的是我们无法再利用早期状态恢复机制来重建以前的 UI。这使得支持 iOS 12 和 13 变得很痛苦。鉴于这个问题主题的新颖性和广泛性,我将在接受答案之前几天给出它。谢谢。 (2认同)

rma*_*ddy 5

根据更多的研究和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")
    }
}
Run Code Online (Sandbox Code Playgroud)

在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")
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,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
    }
}
Run Code Online (Sandbox Code Playgroud)

最后是更新的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
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,与旧状态还原有关的所有代码均已删除。已被替换为NSUserActivity。

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

你也想链调用updateUserActivityState,并restoreUserActivityState根据需要在真正的应用程序的任何子视图。


mat*_*att 5

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

您还希望将呼叫链接到 updateUserActivityState

这错过整点updateUserActivityState,这也是它被称为为你,会自动对所有视图控制器,其userActivity是相同的NSUserActivity返回由现场代表的stateRestorationActivity。

因此,我们自动具有状态保存机制,仅需设计一个匹配的状态恢复机制即可。我将说明我提出的整个体系结构。

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

保存

让我们从保存状态开始。这完全是样板。场景委托创建场景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
}
Run Code Online (Sandbox Code Playgroud)

每个视图控制器都必须使用自己的视图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)
}
Run Code Online (Sandbox Code Playgroud)

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

恢复

这部分比较难。恢复信息将到达session.stateRestorationActivity现场代理。正如最初的问题正确地问到的:现在呢?

剥皮这只猫的方法不只一种,我已经尝试了其中的大多数方法,并选择了这只猫。我的规则是这样的:

  • 每个视图控制器必须具有restorationInfo一个字典属性。在还原过程中创建任何视图控制器时,其创建者(父级)必须将其设置restorationInfo为从userInfo到达的视图控制器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
}
Run Code Online (Sandbox Code Playgroud)

因此,现在唯一的问题是如何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
    }
}
Run Code Online (Sandbox Code Playgroud)

然后,每个视图控制器不仅负责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)
}
Run Code Online (Sandbox Code Playgroud)

最酷的部分是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
}
Run Code Online (Sandbox Code Playgroud)

我认为您可以看到,这只是程度的问题。如果我们在还原过程中要链接更多视图控制器,则它们的工作方式完全相同。

最后的笔记

就像我说的那样,这不是修复猫的唯一方法。但是存在时间安排和责任分配问题,我认为这是最公平的方法。

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

  • 顺便说一句,我有一个示例可以帮助您了解所有这些操作,我的代码基于该示例:https://github.com/mattneub/Programming-iOS-Book-Examples/tree/master/bk2ch06p357StateSaveAndRestoreWithNSUserActivity (2认同)
  • @SAHM 我确信你是对的。我在这里的整个方法是试探性的。问题是苹果拿走了一个非常好的成熟工具,并且没有提供任何替代它的东西。目前还完全不清楚如何进行。也许他们下周会宣布一些事情。希望如此。 (2认同)

mal*_*hal 5

Apple在2019年9月6日发布了此示例应用程序,该应用程序演示了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)
}
Run Code Online (Sandbox Code Playgroud)

在iOS 13上,它使用的configure方法实现了丢失的自动视图控制器层次结构还原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
}
Run Code Online (Sandbox Code Playgroud)

最后,自述文件包含测试建议,但我想补充一下,如果您先启动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"];
Run Code Online (Sandbox Code Playgroud)

  • 您引用的示例的问题在于它微不足道。它无法解决现实世界的复杂性和必须恢复视图控制器及其状态的整个层次结构的后果。基于视图控制器的状态保存和恢复机制是复杂而强大的,Apple 已经弃用了它,并且没有给我们任何东西来替代它。 (7认同)
  • 完全同意@matt。我找不到任何带有严格的视图控制器层次结构以及框架应该如何应用的示例。我用一个单一视图控制器让它工作,但后来就放弃了。现在,我将坚持使用旧的解决方案,并希望苹果能推出更严肃的东西来支持它。 (3认同)