web-dev-qa-db-fra.com

Restauration de l'état de l'interface utilisateur pour une scène dans iOS 13 tout en prenant en charge iOS 12. Pas de story-boards

C'est un peu long mais ce n'est pas anodin et cela prend beaucoup de temps pour démontrer ce problème.

J'essaie de comprendre comment mettre à jour un petit exemple d'application d'iOS 12 à iOS 13. Cet exemple d'application n'utilise aucun story-board (autre que l'écran de lancement). C'est une application simple qui montre un contrôleur de vue avec une étiquette mise à jour par une minuterie. Il utilise la restauration de l'état de sorte que le compteur démarre là où il s'était arrêté. Je veux pouvoir prendre en charge iOS 12 et iOS 13. Dans iOS 13, je souhaite mettre à jour la nouvelle architecture de scène.

Sous iOS 12, l'application fonctionne très bien. Lors d'une nouvelle installation, le compteur commence à 0 et monte. Mettez l'application en arrière-plan, puis redémarrez l'application et le compteur reprend là où il s'était arrêté. La restauration de l'état fonctionne.

Maintenant, j'essaie de faire fonctionner cela sous iOS 13 en utilisant une scène. Le problème que j'ai est de trouver la bonne façon d'initialiser la fenêtre de la scène et de restaurer le contrôleur de navigation et le contrôleur de vue principal sur la scène.

J'ai parcouru autant de documentation Apple que je peux trouver concernant la restauration d'état et les scènes. J'ai regardé des vidéos de la WWDC concernant les fenêtres et les scènes ( 212 - Présentation de Plusieurs fenêtres sur iPad , 258 - Architecture de votre application pour plusieurs fenêtres ). Mais il me semble qu'il me manque un morceau qui rassemble tout cela.

Lorsque j'exécute l'application sous iOS 13, toutes les méthodes de délégué attendues (AppDelegate et SceneDelegate) sont appelées. La restauration d'état restaure le contrôleur de navigation et le contrôleur de vue principal, mais je ne peux pas comprendre comment définir le rootViewController de la fenêtre de la scène, car toute la restauration d'état de l'interface utilisateur est dans l'AppDelegate.

Il semble également y avoir quelque chose lié à un NSUserTask qui devrait être utilisé mais je ne peux pas connecter les points.

Les pièces manquantes semblent appartenir à la méthode willConnectTo de SceneDelegate. Je suis sûr que j'ai également besoin de quelques changements dans stateRestorationActivity de SceneDelegate. Il peut également être nécessaire de modifier le AppDelegate. Je doute que quelque chose dans ViewController doive être changé.


Pour reproduire ce que je fais, créez un nouveau projet iOS avec Xcode 11 (beta 4 pour le moment) à l'aide du modèle d'application Single View. Définissez la cible de déploiement sur iOS 11 ou 12.

Supprimez le storyboard principal. Supprimez les deux références dans Info.plist à Main (une au niveau supérieur et une au plus profond du manifeste de la scène d'application. Mettez à jour les fichiers 3 Swift comme suit).

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)"
    }
}

Exécutez cela sous iOS 11 ou 12 et cela fonctionne très bien.

Vous pouvez l'exécuter sous iOS 13 et sur une nouvelle installation de l'application, vous obtenez l'interface utilisateur. Mais toute exécution ultérieure de l'application donne un écran noir car l'interface utilisateur restaurée via la restauration de l'état n'est pas connectée à la fenêtre de la scène.

Qu'est-ce que je rate? Est-ce qu'il manque juste une ligne ou deux de code ou mon approche complète de la restauration de l'état de la scène iOS 13 est-elle incorrecte?

Gardez à l'esprit qu'une fois que j'ai compris cela, la prochaine étape prendra en charge plusieurs fenêtres. La solution devrait donc fonctionner pour plusieurs scènes, pas pour une seule.

16
rmaddy

Pour prendre en charge la restauration de l'état dans iOS 13, vous devrez encoder suffisamment d'état dans le NSUserActivity:

Utilisez cette méthode pour renvoyer un objet NSUserActivity avec des informations sur les données de votre scène. Enregistrez suffisamment d'informations pour pouvoir récupérer à nouveau ces données après la déconnexion d'UIKit, puis la reconnexion de la scène. Les objets d'activité utilisateur sont destinés à enregistrer ce que l'utilisateur faisait, vous n'avez donc pas besoin d'enregistrer l'état de l'interface utilisateur de votre scène

L'avantage de cette approche est qu'elle peut faciliter la prise en charge du transfert, car vous créez le code nécessaire pour persister et restaurer l'état via les activités utilisateur.

Contrairement à l'approche de restauration d'état précédente où iOS a recréé la hiérarchie du contrôleur de vue pour vous, vous êtes responsable de la création de la hiérarchie de vue pour votre scène dans le délégué de scène.

Si vous avez plusieurs scènes actives, votre délégué sera appelé plusieurs fois pour enregistrer l'état et plusieurs fois pour restaurer l'état; Rien de spécial n'est nécessaire.

Les modifications que j'ai apportées à votre code sont les suivantes:

AppDelegate.Swift

Désactivez la restauration d'état "héritée" sur iOS 13 et versions ultérieures:

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

Créez une activité utilisateur si nécessaire et utilisez-la pour recréer le contrôleur de vue. Notez que vous êtes responsable de la création de la hiérarchie de vues dans les cas normaux et de restauration.

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

Ajoutez la prise en charge de l'enregistrement et du chargement à partir d'un 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

Sur la base de plus de recherches et de suggestions très utiles de la réponse de Paulw11 J'ai trouvé une approche qui fonctionne pour iOS 13 et iOS 12 (et versions antérieures) sans duplication de code et en utilisant la même approche pour toutes les versions d'iOS.

Notez que bien que la question d'origine et cette réponse n'utilisent pas de storyboards, la solution serait essentiellement la même. Les seules différences sont qu'avec les storyboards, AppDelegate et SceneDelegate n'auraient pas besoin du code pour créer la fenêtre et le contrôleur de vue racine. Et bien sûr, le ViewController n'aurait pas besoin de code pour créer ses vues.

L'idée de base est de migrer le code iOS 12 pour qu'il fonctionne de la même manière que iOS 13. Cela signifie que l'ancienne restauration d'état n'est plus utilisée. NSUserTask est utilisé pour enregistrer et restaurer l'état. Cette approche présente plusieurs avantages. Il permet au même code de fonctionner pour toutes les versions d'iOS, il vous rapproche vraiment de la prise en charge sans pratiquement aucun effort supplémentaire, et il vous permet de prendre en charge plusieurs scènes de fenêtre et une restauration d'état complète en utilisant le même code de base.

Voici la mise à jour 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")
    }
}

Sous iOS 12 et versions antérieures, le processus de restauration d'état standard est désormais uniquement utilisé pour enregistrer/restaurer le NSUserActivity. Il n'est plus utilisé pour conserver la hiérarchie des vues.

Puisque NSUserActivity n'est pas conforme à NSCoding, une classe wrapper est utilisée.

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")
    }
}

Notez que des propriétés supplémentaires de NSUserActivity peuvent être nécessaires en fonction de vos besoins.

Voici la mise à jour de 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
    }
}

Et enfin le ViewController.Swift mis à jour:

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

Notez que tout le code lié à la restauration de l'ancien état a été supprimé. Il a été remplacé par l'utilisation de NSUserActivity.

Dans une application réelle, vous stockeriez toutes sortes d'autres détails dans l'activité utilisateur nécessaire pour restaurer complètement l'état de l'application au redémarrage ou pour prendre en charge le transfert. Ou stockez un minimum de données nécessaires pour lancer une nouvelle scène de fenêtre.

Vous souhaitez également chaîner les appels à updateUserActivityState et restoreUserActivityState à toutes les vues enfants, selon les besoins, dans une application réelle.

8
rmaddy

C'est, me semble-t-il, le principal défaut de la structure du réponses présentées jusqu'à présent :

Vous souhaitez également chaîner les appels vers updateUserActivityState

Cela manque tout le point de updateUserActivityState, c'est-à-dire qu'il est appelé automatiquement pour vous, pour tous les contrôleurs de vue dont userActivity est le identique comme NSUserActivity retourné par le stateRestorationActivity du délégué de la scène.

Ainsi, nous avons automatiquement un mécanisme de sauvegarde de l'état, et il ne reste plus qu'à concevoir un mécanisme de restauration de l'état correspondant. Je vais illustrer toute une architecture que j'ai imaginée.

REMARQUE: Cette discussion ignore plusieurs fenêtres et ignore également l'exigence d'origine de la question, à savoir que nous soyons compatibles avec l'enregistrement d'état basé sur le contrôleur de vue iOS 12 et restauration. Mon objectif ici est seulement de montrer comment faire la sauvegarde et la restauration de l'état dans iOS 13 en utilisant NSUserActivity. Cependant, seules des modifications mineures sont nécessaires afin de plier cela dans une application à fenêtres multiples, donc je pense que cela répond adéquatement à la question d'origine.

Économie

Commençons par l'économie d'état. Ceci est entièrement passe-partout. Le délégué de scène crée la scène userActivity ou lui transmet l'activité de restauration reçue et la renvoie comme sa propre activité utilisateur:

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
}

Chaque contrôleur de vue doit utiliser son propre viewDidAppear pour partager cet objet d'activité utilisateur. De cette façon, son propre updateUserActivityState sera appelé automatiquement lorsque nous irons en arrière-plan, et il a une chance de contribuer au pool global des informations utilisateur:

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)
}

C'est tout! Si chaque contrôleur de vue fait cela, alors chaque contrôleur de vue qui est vivant au moment où nous entrons en arrière-plan a la possibilité de contribuer aux informations utilisateur de l'activité de l'utilisateur qui arrivera la prochaine fois que nous lancerons.

Restauration

Cette partie est plus difficile. Les informations de restauration arriveront sous la forme session.stateRestorationActivity dans le délégué de scène. Comme la question d'origine le demande à juste titre: maintenant quoi?

Il y a plus d'une façon de dépouiller ce chat, et j'ai essayé la plupart d'entre eux et j'ai choisi celui-ci. Ma règle est la suivante:

  • Chaque contrôleur de vue doit avoir une propriété restorationInfo qui est un dictionnaire. Lorsqu'un contrôleur de vue est créé pendant la restauration, son créateur (parent) doit définir ce restorationInfo sur le userInfo qui est arrivé de session.stateRestorationActivity.

  • Ce userInfo doit être copié dès le début, car il sera effacé de l'activité enregistrée la première fois que updateUserActivityState est appelé (c'est la partie qui m'a vraiment rendu fou de travailler sur ce architecture).

La partie intéressante est que si nous faisons cela correctement, le restorationInfo est défini avantviewDidLoad, et ainsi le contrôleur de vue peut se configurer en fonction des informations qu'il mettre dans le dictionnaire sur l'enregistrement.

Chaque contrôleur de vue doit également supprimer son propre restorationInfo lorsqu'il en a fini avec lui, de peur qu'il ne l'utilise à nouveau pendant la durée de vie de l'application. Il ne doit être utilisé qu'une seule fois, lors du lancement.

Il faut donc changer notre passe-partout:

var restorationInfo :  [AnyHashable : Any]?
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    self.userActivity = self.view.window?.windowScene?.userActivity
    self.restorationInfo = nil
}

Alors maintenant, le seul problème est la chaîne de définition du restorationInfo de chaque contrôleur de vue. La chaîne commence par le délégué de scène, qui est responsable de la définition de cette propriété dans le contrôleur de vue racine:

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

Chaque contrôleur de vue est alors responsable non seulement de se configurer dans son viewDidLoad sur la base du restorationInfo, mais aussi de chercher à voir s'il était le parent/présentateur d'un autre contrôleur de vue. Si tel est le cas, il doit créer et présenter/Push/quel que soit ce contrôleur de vue, en veillant à transmettre le restorationInfo avant que le contrôleur de vue enfant viewDidLoad s'exécute.

Si chaque contrôleur de vue le fait correctement, toute l'interface et l'état seront restaurés!

Un peu plus d'exemple

Supposons que nous ayons seulement deux contrôleurs de vue possibles: RootViewController et PresentedViewController. Soit RootViewController présentait PresentedViewController au moment où nous avons été mis en arrière-plan, soit ce n'était pas le cas. Quoi qu'il en soit, ces informations ont été écrites dans le dictionnaire d'informations.

Voici donc ce que fait 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)
}

La partie intéressante est que le PresentedViewController fait exactement la même chose!

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
}

Je pense que vous pouvez voir qu'à ce stade, ce n'est qu'une question de degré. Si nous avons plus de contrôleurs de vue à chaîner pendant le processus de restauration, ils fonctionnent tous exactement de la même manière.

Notes finales

Comme je l'ai dit, ce n'est pas le seul moyen de dépouiller le chat de restauration. Mais il y a des problèmes de calendrier et de répartition des responsabilités, et je pense que c'est l'approche la plus équitable.

En particulier, je ne suis pas d'accord avec l'idée que le délégué de la scène devrait être responsable de la restauration complète de l'interface. Il lui faudrait en savoir trop sur les détails de la façon d'initialiser chaque contrôleur de vue le long de la ligne, et il y a de sérieux problèmes de synchronisation qui sont difficiles à résoudre de manière déterministe. Mon approche imite en quelque sorte l'ancienne restauration basée sur le contrôleur de vue, rendant chaque contrôleur de vue responsable de son enfant de la même manière qu'il le serait normalement.

8
matt

Le 6 septembre 2019 Apple publié cet exemple d'application qui montre la restauration de l'état d'iOS 13 avec une compatibilité descendante avec iOS 12.

À partir de Readme.md

L'échantillon prend en charge deux approches différentes de préservation de l'état. Dans iOS 13 et versions ultérieures, les applications enregistrent l'état de chaque scène de fenêtre à l'aide d'objets NSUserActivity. Dans iOS 12 et versions antérieures, les applications préservent l'état de leur interface utilisateur en enregistrant et en restaurant la configuration des contrôleurs de vue.

Le fichier Lisezmoi explique en détail comment cela fonctionne - l'astuce de base est que sur iOS 12, il code l'objet d'activité (disponible dans iOS 12 à d'autres fins) dans l'ancienne méthode encodeRestorableState.

override func encodeRestorableState(with coder: NSCoder) {
    super.encodeRestorableState(with: coder)

    let encodedActivity = NSUserActivityEncoder(detailUserActivity)
    coder.encode(encodedActivity, forKey: DetailViewController.restoreActivityKey)
}

Et sur iOS 13, il implémente la restauration de la hiérarchie du contrôleur de vue automatique manquante à l'aide de la méthode configure de SceneDelegate.

func configure(window: UIWindow?, with activity: NSUserActivity) -> Bool {
    if let detailViewController = DetailViewController.loadFromStoryboard() {
        if let navigationController = window?.rootViewController as? UINavigationController {
            navigationController.pushViewController(detailViewController, animated: false)
            detailViewController.restoreUserActivityState(activity)
            return true
        }
    }
    return false
}

Enfin, le fichier Lisezmoi comprend des conseils de test, mais j'aimerais ajouter si vous lancez d'abord le simulateur Xcode 10.2, par exemple iPhone 8 Plus, puis lancez Xcode 11, vous aurez l'iPhone 8 Plus (12.4) en option et vous pourrez découvrir le comportement rétrocompatible. J'aime également utiliser ces valeurs par défaut, la seconde permet à l'archive de restauration de survivre aux plantages:

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