これは少し長いですが、ささいなことではなく、この問題を実証するには多くの時間がかかります。
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
に関連するものもあるようですが、ドットを接続できません。
不足している部分はwillConnectTo
のSceneDelegate
メソッドにあるようです。 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つだけでなく、複数のシーンでも機能するはずです。
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
}
より多くの調査と 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
の使用に置き換えられました。
実際のアプリでは、再起動時にアプリの状態を完全に復元したり、ハンドオフをサポートしたりするために必要なユーザーアクティビティに、他のあらゆる種類の詳細を保存します。または、新しいウィンドウシーンを起動するために必要な最小限のデータを保存します。
また、updateUserActivityState
およびrestoreUserActivityState
への呼び出しを、実際のアプリで必要に応じて任意の子ビューにチェーンすることもできます。
これは、私には思えます これまでに提示された回答 の構造の主要な欠陥です:
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を使用する必要があります。このように、バックグラウンドに入ると、独自のupdateUserActivityState
がautomaticallyと呼ばれ、ユーザーのグローバルプールに貢献する機会があります情報:
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
がbeforeviewDidLoad
に設定されるため、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
}
この時点では、程度の問題にすぎないことがお分かりでしょう。復元プロセス中にチェーンするビューコントローラーがさらにある場合、それらはすべてまったく同じように動作します。
私が言ったように、これは回復猫の皮をむく唯一の方法ではありません。しかし、タイミングと責任の配分の問題があり、これが最も公平なアプローチだと思います。
特に、シーンデリゲートがインターフェイスの全体的な復元に責任を持つべきだという考えには同意しません。ラインに沿って各ビューコントローラーを初期化する方法の詳細について多くを知る必要があります。決定論的な方法で克服するのが難しい深刻なタイミングの問題があります。私のアプローチは、古いビューコントローラーベースの復元を模倣して、各ビューコントローラーが通常どおりに子を担当するようにします。
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
の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
}
最後に、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"];