web-dev-qa-db-ja.com

iOS 12をサポートしながら、iOS 13のシーンのUI状態の復元。ストーリーボードなし

これは少し長いですが、ささいなことではなく、この問題を実証するには多くの時間がかかります。

IOS 12からiOS 13に小さなサンプルアプリを更新する方法を見つけようとしています。このサンプルアプリはストーリーボード(起動画面以外)を使用していません。これは、タイマーによって更新されるラベルが付いた1つのビューコントローラーを表示するシンプルなアプリです。状態の復元を使用するため、カウンターは中断したところから開始します。 iOS 12とiOS 13をサポートできるようにしたいと考えています。iOS13では、新しいシーンアーキテクチャに更新したいと考えています。

IOS 12では、アプリは問題なく動作します。新規インストールでは、カウンターは0から始まり、増加します。アプリをバックグラウンドに配置してからアプリを再起動すると、カウンターは中断したところから続行します。状態復旧は全て動作します。

今、私はそれをシーンを使用してiOS 13で動作させるようにしています。私が抱えている問題は、シーンのウィンドウを初期化し、ナビゲーションコントローラーとメインビューコントローラーをシーンに復元する正しい方法を見つけることです。

状態の復元とシーンに関連するAppleのドキュメントをできるだけ多く読みました。ウィンドウとシーンに関連するWWDCビデオを視聴しました( 212-紹介iPad上の複数のウィンドウ258-複数のウィンドウ用にアプリを設計する )。しかし、それをすべてまとめた部分が欠けているようです。

IOS 13でアプリを実行すると、予想されるすべてのデリゲートメソッド(AppDelegateとSceneDelegateの両方)が呼び出されます。状態の復元により、ナビゲーションコントローラーとメインビューコントローラーが復元されますが、UIの状態の復元はすべてAppDelegateにあるため、シーンのウィンドウのrootViewControllerを設定する方法がわかりません。

使用すべきNSUserTaskに関連するものもあるようですが、ドットを接続できません。

不足している部分はwillConnectToSceneDelegateメソッドにあるようです。 stateRestorationActivity/SceneDelegateにも変更が必要だと思います。 AppDelegateにも変更が必要な場合があります。 ViewControllerのすべてを変更する必要があるとは思いません。


私がやっていることを再現するには、シングルビューアプリテンプレートを使用して、Xcode 11(現時点ではベータ4)で新しいiOSプロジェクトを作成します。展開ターゲットをiOS 11または12に設定します。

メインのストーリーボードを削除します。 Info.plistからMainへの2つの参照を削除します(1つは最上位レベル、もう1つはアプリケーションシーンマニフェストの奥にあります。3Swiftファイルを次のように更新します。

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がシーンのウィンドウに接続されていないため、以降のアプリの実行では黒い画面が表示されます。

何が欠けていますか?これは1行または2行のコードが不足しているだけですか、それともiOS 13のシーン状態の復元に対する私の全体的なアプローチが間違っていますか?

これを理解したら、次のステップは複数のウィンドウをサポートすることになることに注意してください。したがって、ソリューションは1つだけでなく、複数のシーンでも機能するはずです。

16
rmaddy

IOS 13で状態の復元をサポートするには、十分な状態をNSUserActivityにエンコードする必要があります。

このメソッドを使用して、シーンのデータに関する情報を含むNSUserActivityオブジェクトを返します。 UIKitを切断してシーンを再接続した後、そのデータを再度取得できるように十分な情報を保存します。ユーザーアクティビティオブジェクトは、ユーザーが行っていたことを記録するためのものなので、シーンのUIの状態を保存する必要はありません。

このアプローチの利点は、ユーザーアクティビティを介して状態を永続化および復元するために必要なコードを作成しているため、ハンドオフをサポートしやすくなることです。

IOSがView Controller階層を再作成する以前の状態復元アプローチとは異なり、シーンデリゲートでシーンのビュー階層を作成するのはユーザーの責任です。

複数のアクティブなシーンがある場合、デリゲートは複数回呼び出されて状態が保存され、複数回呼び出されて状態が復元されます。特別なことは必要ありません。

コードに加えた変更は次のとおりです。

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
}
13
Paulw11

より多くの調査と 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の保存/復元のみに使用されるようになりました。ビュー階層を永続化するために使用されることはありません。

NSUserActivityNSCodingに準拠していないため、ラッパークラスが使用されます。

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の使用に置き換えられました。

実際のアプリでは、再起動時にアプリの状態を完全に復元したり、ハンドオフをサポートしたりするために必要なユーザーアクティビティに、他のあらゆる種類の詳細を保存します。または、新しいウィンドウシーンを起動するために必要な最小限のデータを保存します。

また、updateUserActivityStateおよびrestoreUserActivityStateへの呼び出しを、実際のアプリで必要に応じて任意の子ビューにチェーンすることもできます。

8
rmaddy

これは、私には思えます これまでに提示された回答 の構造の主要な欠陥です:

updateUserActivityStateへの呼び出しをチェーンすることもできます

これはupdateUserActivityStateのすべてのポイントを逃しています。つまり、userActivityがすべてのビューコントローラーに対して自動的に呼び出されますです。sameは、シーンデリゲートのstateRestorationActivityから返されるNSUserActivityと同じです。

したがって、自動的に状態を保存するメカニズムがあり、それに合わせて状態を復元するメカニズムを考案するだけです。私が思いついたアーキテクチャ全体を説明します。

注:この説明では複数のウィンドウが無視され、質問の元の要件も無視されます。これは、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
}

すべてのView Controllerは、そのユーザーアクティビティオブジェクトに独自のviewDidAppear toshareを使用する必要があります。このように、バックグラウンドに入ると、独自のupdateUserActivityStateautomaticallyと呼ばれ、ユーザーのグローバルプールに貢献する機会があります情報:

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プロパティが必要です。復元中にビューコントローラが作成されると、その作成者(親)はそのrestorationInfosession.stateRestorationActivityから到着したuserInfoに設定する必要があります。

  • このuserInfoは、最初にコピーする必要があります。これは、最初にupdateUserActivityStateが呼び出されたときに、保存されたアクティビティから消去されるためです(これは、私が本当にこれを解決するのに夢中になった部分です)建築)。

クールな部分は、これを正しく行うと、restorationInfobeforeviewDidLoadに設定されるため、View Controllerがそれ自体を構成できるということです保存時に辞書に入れられた情報に基づきます。

また、各View Controllerは、アプリの有効期間中に再度使用しないように、delete独自のrestorationInfoを使用する必要があります。起動時に1回だけ使用する必要があります。

したがって、ボイラープレートを変更する必要があります。

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を確実に渡してください。

すべてのView Controllerがこれを正しく行うと、インターフェース全体と状態が復元されます!

もう少し例

RootViewControllerとPresentedViewControllerの2つのビューコントローラーしかないと仮定します。バックグラウンドにいたときに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
}

この時点では、程度の問題にすぎないことがお分かりでしょう。復元プロセス中にチェーンするビューコントローラーがさらにある場合、それらはすべてまったく同じように動作します。

最終メモ

私が言ったように、これは回復猫の皮をむく唯一の方法ではありません。しかし、タイミングと責任の配分の問題があり、これが最も公平なアプローチだと思います。

特に、シーンデリゲートがインターフェイスの全体的な復元に責任を持つべきだという考えには同意しません。ラインに沿って各ビューコントローラーを初期化する方法の詳細について多くを知る必要があります。決定論的な方法で克服するのが難しい深刻なタイミングの問題があります。私のアプローチは、古いビューコントローラーベースの復元を模倣して、各ビューコントローラーが通常どおりに子を担当するようにします。

8
matt

2019年9月6日Apple release this sample app これは、iOS 12との下位互換性を備えたiOS 13の状態の復元を示しています。

From Readme.md

サンプルは、2つの異なる状態保存アプローチをサポートしています。 iOS 13以降では、アプリはNSUserActivityオブジェクトを使用して各ウィンドウシーンの状態を保存します。 iOS 12以前では、アプリはビューコントローラーの構成を保存および復元することにより、ユーザーインターフェイスの状態を保持します。

Readmeはそれがどのように機能するかを詳しく説明しています。基本的なトリックは、iOS 12では古いencodeRestorableStateメソッドでアクティビティオブジェクト(別の目的で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のc​​onfigureメソッドを使用して、欠落している自動ビューコントローラー階層復元を実装します。

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
}

最後に、Readmeにはテストのアドバイスが含まれていますが、Xcode 10.2シミュレーターを最初に起動する場合は追加します。 iPhone 8 Plusを起動してXcode 11を起動すると、オプションとしてiPhone 8 Plus(12.4)が提供され、下位互換性のある動作を体験できます。また、これらのユーザーデフォルトを使用することも好きです。2番目の方法では、復元アーカイブがクラッシュから生き残ることができます。

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