【问题标题】:UI state restoration for a scene in iOS 13 while still supporting iOS 12. No storyboards在 iOS 13 中恢复场景的 UI 状态,同时仍支持 iOS 12。无情节提要
【发布时间】:2019-11-29 11:08:35
【问题描述】:

这有点长,但也不是小事,证明这个问题需要很多时间。

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

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

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

我已经阅读了尽可能多的与状态恢复和场景相关的 Apple 文档。我看过与窗口和场景相关的 WWDC 视频(212 - Introducing Multiple Windows on iPad258 - Architecting Your App for Multiple Windows)。但我似乎缺少将它们组合在一起的部分。

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

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

缺失的部分似乎在SceneDelegatewillConnectTo 方法中。我确定我还需要对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 场景状态恢复的整个方法都错了?

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

【问题讨论】:

  • 能否请您编辑问题以澄清您的应用是适用于 iPhone、iPad 还是通用的?
  • @malhal 这与问题无关。为什么这很重要?
  • 场景是可选的,因此如果它仅适用于 iPhone,则不要并继续正常使用状态恢复。场景甚至不应该在 iOS 中,而应该在 iPadOS 中。
  • @malhal 关于仅限 iPhone 的应用程序的要点。所以假设这是关于 iPad 或通用应用程序的。

标签: ios ios13 state-restoration uiscene


【解决方案1】:

2019 年 9 月 6 日,Apple 发布了 this sample app,演示了 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 的 configure 方法实现了缺少的自动视图控制器层次结构恢复。

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"];

【讨论】:

  • 你举的例子的问题是它微不足道。它无法应对现实世界的复杂性和必须恢复视图控制器及其状态的整个层次结构的后果。基于视图控制器的状态保存和恢复机制是复杂而强大的,Apple 已经弃用它,但没有给我们任何东西来替代它。
  • 完全同意@matt。我找不到任何具有严重层次结构的视图控制器以及应该如何应用框架的示例。我用一个单一的视图控制器让它工作,但后来就放弃了。目前,我将坚持使用旧版解决方案,并希望 Apple 推出更严肃的解决方案来支持它。
  • 在适应性方面恢复层次结构是失败的。例如。使用拆分视图,如果状态编码为折叠紧凑宽度并在分离常规宽度时恢复,则在启动时如果首先将导致折叠的特征恢复为假紧凑宽度,尽管设备实际上是常规宽度,但需要恢复折叠。此外,您必须处理大小类编码/恢复的所有其他排列。引爆点是 13 的基于面板的拆分控制器 - 状态恢复调用与拆分委托的顺序都发生了变化,使其不可能!用户活动是一个新的开始尝试一下。
  • 仅供参考,这是因为折叠和分离时的恢复路径不同,因此您将有重复的 VC 或尝试搜索它们,这是丑陋代码的日志。
【解决方案2】:

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

您还希望将调用链接到updateUserActivityState

这忽略了updateUserActivityState 的全部意义,即对于所有userActivity相同 的视图控制器,它会自动为您调用场景委托的stateRestorationActivity返回的NSUserActivity。

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

注意:此讨论忽略了多个窗口,也忽略了问题的原始要求,即我们兼容 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 属性,它是一个字典。在恢复期间创建任何视图控制器时,其创建者(父级)必须将 restorationInfo 设置为从 session.stateRestorationActivity 到达的 userInfo

  • 这个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
    }
}

然后,每个视图控制器不仅负责根据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
}

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

结语

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

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

【讨论】:

  • 我需要更多时间来消化这个彻底的答案(谢谢),但我想澄清一件事,因为您在答案中引用了它。当我说(在我的回答中)“您还希望将调用链接到updateUserActivityState时,我指的是每个视图控制器都需要在它们自己的子视图控制器上调用它。我从来没有暗示场景委托中有任何逻辑。
  • 我是这么理解的。我的观点是,它旨在利用 updateUserActivityState 被自动调用这一事实。如果你做对了,你应该永远需要自己调用它。这样做也没有意义,因为运行时以一种特殊的方式调用它,首先将用户信息清零。这是一个事件,而不是你所说的。
  • 顺便说一下,我有一个例子可以帮助你看到这一切,我的代码就是基于这个例子:github.com/mattneub/Programming-iOS-Book-Examples/tree/master/…
  • @matt 这个例子和你的书(我买的)非常有帮助。这增加了额外的信息,表明您应该在 viewWillLayoutSubviews 中执行恢复“无动画”序列,这也非常有帮助。不过,我认为,如果在 viewWillLayoutSubviews 中执行 segue,我们可能还想在该方法中设置 self.userActivity,在执行 segue 之前以及在 didFirstWillLayout 条件之前/之外; (下续)
  • @SAHM 我相信你是对的。我在这里的整个方法是暂定的。问题是苹果已经拿走了一个非常好的成熟工具,并且没有提供任何替代它的东西。完全不清楚如何进行。也许他们会在下周宣布一些事情。希望如此。
【解决方案3】:

基于来自the answer by 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 的调用链接到任何子视图。

【讨论】:

  • 在呈现设置自己活动的非全屏视图控制器时,使用 viewDidAppear 设置窗口的用户活动将不起作用。关闭非全屏视图控制器不会在呈现视图控制器上调用 viewDidAppear(因为呈现视图控制器没有消失)。 iOS 13 的卡片式演示打破了围绕当前视图控制器上的视图生命周期方法的一些(总是有些不安全的)假设。
  • @Geoff 我理解这些变化。它们与我发布的代码无关。我只关心视图控制器何时显示以及何时被关闭。其他视图控制器正在做什么不是这个视图控制器的责任。当然,每个应用程序的需求是不同的。根据需要进行调整。
  • @rmaddy 我正在尝试排序如何在 iOS13 中进行状态保存/恢复工作,将您的解决方案应用于 Xcode 11 中的 Master/Detail 应用程序模板。我遇到的问题是那个窗口? stateRestorationActivity(for scene: ) 中的 .userActivity 始终为零,因此根本不会开始保存状态。示例代码:dropbox.com/s/j7syc6pueyz8f7q/ScrollRestoration.zip?dl=0
  • @FrancoSolerio 请发布您自己的问题以及所有相关详细信息。这是获得帮助的最佳方式。确保您的问题直接在您的问题中包含相关代码(作为文本)。
【解决方案4】:

为了支持 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
}

【讨论】:

  • 当我在发布后对此进行了进一步研究时,我意识到 iOS 13 中的状态恢复更像是切换功能。很遗憾,我们不能再利用早期状态恢复机制来重建以前的 UI。这使得支持 iOS 12 和 13 变得很痛苦。鉴于此问题主题的新颖性和范围广泛,我将在接受答案前几天给出它。谢谢。
猜你喜欢
  • 1970-01-01
  • 2012-12-31
  • 2019-08-28
  • 2020-02-12
  • 1970-01-01
  • 2012-11-08
  • 1970-01-01
  • 2014-09-07
  • 1970-01-01
相关资源
最近更新 更多