ios - 在 iOS 13 中恢复场景的 UI 状态,同时仍然支持 iOS 12。没有 Storyboard

标签 ios ios13 state-restoration uiscene

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

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

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

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

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

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

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

丢失的部分似乎在 willConnectToSceneDelegate的方法.我确定我还需要对 stateRestorationActivity 进行一些更改的 SceneDelegate . AppDelegate也可能需要改动.我怀疑 ViewController需要改变。

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

删除主 Storyboard。删除 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")
    }
}

View Controller .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 场景状态恢复的整个方法是错误的?

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

最佳答案

要在 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 为您重新创建 View Controller 层次结构的状态恢复方法不同,您负责在场景委托(delegate)中为您的场景创建 View 层次结构。

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

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

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

在需要时创建用户事件并使用它来重新创建 View Controller 。请注意,您负责在正常和还原情况下创建 View 层次结构。
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 - 在 iOS 13 中恢复场景的 UI 状态,同时仍然支持 iOS 12。没有 Storyboard,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57129668/

相关文章:

iphone - 如何改变框架的旋转点

ios - 如何在不失去对旧 iOS 版本的支持的情况下采用暗模式?

ios - 为 restorationIdentifier 提供默认值

ios - 3D Touch 和状态恢复的问题

ios - 呈现 View Controller 时状态恢复不起作用?

ios - 如何在世界追踪中显示人脸

ios/xcode/核心数据 : Data model for many to many relationships

ios - 无效更新 : invalid number of sections

ios - UIPresentationController delegate如何设置

swift - 如何使 Onboarding 与 iOS13 中的 Scene Delegate 配合使用?