Восстановление состояния пользовательского интерфейса для сцены в iOS 13 при поддержке iOS 12. Раскадровки нет.

Это немного долго, но это нетривиально, и для демонстрации этой проблемы требуется много времени.

Я пытаюсь понять, как обновить небольшой пример приложения с iOS 12 до iOS 13. В этом примере приложения не используются раскадровки (кроме экрана запуска). Это простое приложение, которое показывает один контроллер представления с меткой, которая обновляется таймером. Он использует восстановление состояния, поэтому счетчик запускается с того места, где он остановился. Я хочу иметь возможность поддерживать iOS 12 и iOS 13. В iOS 13 я хочу перейти на новую архитектуру сцены.

Под iOS 12 приложение работает нормально. При новой установке счетчик начинается с 0 и увеличивается. Переведите приложение в фоновый режим, а затем перезапустите приложение, и счетчик продолжится с того места, где он остановился. Государственная реставрация все работает.

Теперь я пытаюсь заставить это работать под iOS 13, используя сцену. Проблема, с которой я столкнулся, заключается в том, чтобы найти правильный способ инициализировать окно сцены и восстановить контроллер навигации и контроллер основного представления в сцене.

Я просмотрел как можно больше документации Apple, связанной с восстановлением состояния и сценами. Я смотрел видеоролики WWDC, связанные с окнами и сценами (212 - Введение в несколько Windows на iPad, 258 - Создание архитектуры вашего приложения для нескольких окон). Но мне, кажется, не хватает того, что объединяет все воедино.

Когда я запускаю приложение под iOS 13, вызываются все ожидаемые методы делегата (как AppDelegate, так и SceneDelegate). Восстановление состояния восстанавливает контроллер навигации и контроллер основного представления, но я не могу понять, как установитьrootViewController окна сцены, так как все восстановление состояния пользовательского интерфейса находится в AppDelegate.

Также, кажется, что-то связано с NSUserTask это следует использовать, но я не могу соединить точки.

Кажется, что недостающие части находятся в willConnectTo метод SceneDelegate. Я уверен, что мне тоже нужны изменения вstateRestorationActivity из SceneDelegate. Также могут потребоваться изменения вAppDelegate. Я сомневаюсь ни в чемViewController нужно изменить.


Чтобы воспроизвести то, что я делаю, создайте новый проект iOS с Xcode 11 (на данный момент бета 4), используя шаблон приложения для единого представления. Установите цель развертывания iOS 11 или 12.

Удалите основную раскадровку. Удалите две ссылки в Info.plist на Main (одну на верхнем уровне и одну глубоко внутри манифеста сцены приложения. Обновите 3 быстрых файла следующим образом.

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, и при новой установке приложения вы получите пользовательский интерфейс. Но любой последующий запуск приложения дает черный экран, потому что пользовательский интерфейс, восстановленный с помощью восстановления состояния, не связан с окном сцены.

Что мне не хватает? Это просто не хватает одной или двух строк кода или весь мой подход к восстановлению состояния сцены iOS 13 неверен?

Имейте в виду, что как только я пойму это, следующим шагом будет поддержка нескольких окон. Таким образом, решение должно работать для нескольких сцен, а не только для одной.

4 ответа

Решение

Для поддержки восстановления состояния в iOS 13 вам нужно будет закодировать достаточно состояния в NSUserActivity:

Используйте этот метод, чтобы вернуть объект NSUserActivity с информацией о данных вашей сцены. Сохраните достаточно информации, чтобы иметь возможность снова получить эти данные после того, как UIKit отключится, а затем снова подключит сцену. Объекты активности пользователя предназначены для записи того, что делал пользователь, поэтому вам не нужно сохранять состояние пользовательского интерфейса вашей сцены.

Преимущество этого подхода состоит в том, что он может упростить поддержку передачи обслуживания, поскольку вы создаете код, необходимый для сохранения и восстановления состояния посредством действий пользователя.

В отличие от предыдущего подхода к восстановлению состояния, когда iOS воссоздала для вас иерархию контроллера представления, вы несете ответственность за создание иерархии представлений для своей сцены в делегате сцены.

Если у вас есть несколько активных сцен, ваш делегат будет вызываться несколько раз для сохранения состояния и несколько раз для восстановления состояния; Ничего особенного не нужно.

В ваш код я внес следующие изменения:

AppDelegate.swift

Отключите восстановление "устаревшего" состояния на iOS 13 и новее:

func application(_ application: UIApplication, viewControllerWithRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
    if #available(iOS 13, *) {

    } else {
        print("AppDelegate viewControllerWithRestorationIdentifierPath")

        // If this is for the nav controller, restore it and set it as the window's root
        if identifierComponents.first == "RootNC" {
            let nc = UINavigationController()
            nc.restorationIdentifier = "RootNC"
            self.window?.rootViewController = nc

            return nc
        }
    }
    return nil
}

func application(_ application: UIApplication, willEncodeRestorableStateWith coder: NSCoder) {
    print("AppDelegate willEncodeRestorableStateWith")
    if #available(iOS 13, *) {

    } else {
    // Trigger saving of the root view controller
        coder.encode(self.window?.rootViewController, forKey: "root")
    }
}

func application(_ application: UIApplication, didDecodeRestorableStateWith coder: NSCoder) {
    print("AppDelegate didDecodeRestorableStateWith")
}

func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
    print("AppDelegate shouldSaveApplicationState")
    if #available(iOS 13, *) {
        return false
    } else {
        return true
    }
}

func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
    print("AppDelegate shouldRestoreApplicationState")
    if #available(iOS 13, *) {
        return false
    } else {
        return true
    }
}

SceneDelegate.swift

При необходимости создайте действие пользователя и используйте его для воссоздания контроллера представления. Обратите внимание, что вы несете ответственность за создание иерархии представлений как в обычном случае, так и в случае восстановления.

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    print("SceneDelegate willConnectTo")

    guard let winScene = (scene as? UIWindowScene) else { return }

    // Got some of this from WWDC2109 video 258
    window = UIWindow(windowScene: winScene)

    let vc = ViewController()

    if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
        vc.continueFrom(activity: activity)
    }

    let nc = UINavigationController(rootViewController: vc)
    nc.restorationIdentifier = "RootNC"

    self.window?.rootViewController = nc
    window?.makeKeyAndVisible()


}

func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
    print("SceneDelegate stateRestorationActivity")

    if let nc = self.window?.rootViewController as? UINavigationController, let vc = nc.viewControllers.first as? ViewController {
        return vc.continuationActivity
    } else {
        return nil
    }

}

ViewController.swift

Добавить поддержку сохранения и загрузки из NSUserActivity.

var continuationActivity: NSUserActivity {
    let activity = NSUserActivity(activityType: "restoration")
    activity.persistentIdentifier = UUID().uuidString
    activity.addUserInfoEntries(from: ["Count":self.count])
    return activity
}

func continueFrom(activity: NSUserActivity) {
    let count = activity.userInfo?["Count"] as? Int ?? 0
    self.count = count
}

Это, как мне кажется, главный недостаток в структуре представленных ответов:

Вы также можете связать вызовы с updateUserActivityState

Это упускает из виду весь смысл updateUserActivityState, то есть он вызывается автоматически для всех контроллеров представления, чьиuserActivityэто же как NSUserActivity возвращенная сцена делегатаstateRestorationActivity.

Таким образом, у нас автоматически появляется механизм сохранения состояния, и остается только разработать соответствующий механизм восстановления состояния. Я проиллюстрирую всю архитектуру, которую придумал.

ПРИМЕЧАНИЕ. Это обсуждение игнорирует несколько окон, а также игнорирует исходное требование вопроса, что мы должны быть совместимы с сохранением и восстановлением состояния на основе контроллера представления iOS 12. Моя цель здесь - только показать, как сохранять и восстанавливать состояние в iOS 13 с помощью NSUserActivity. Однако необходимы лишь незначительные изменения, чтобы превратить это в многооконное приложение, поэтому я думаю, что он адекватно отвечает на исходный вопрос.

Экономия

Начнем с государственной экономии. Это полностью шаблон. Делегат сцены либо создает сценуuserActivity или передает полученное действие восстановления в него и возвращает его как собственное действие пользователя:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    guard let scene = (scene as? UIWindowScene) else { return }
    scene.userActivity =
        session.stateRestorationActivity ??
        NSUserActivity(activityType: "restoration")
}
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
    return scene.userActivity
}

Каждый контроллер представления должен использовать свой собственный viewDidAppearчтобы поделиться этим объектом активности пользователя. Таким образом, свой собственныйupdateUserActivityStateбудет вызываться автоматически, когда мы перейдем в фоновый режим, и у него есть шанс внести свой вклад в глобальный пул пользовательской информации:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    self.userActivity = self.view.window?.windowScene?.userActivity
}
// called automatically at saving time!
override func updateUserActivityState(_ activity: NSUserActivity) {
    super.updateUserActivityState(activity)
    // gather info into `info`
    activity.addUserInfoEntries(from: info)
}

Вот и все! Если каждый контроллер представления делает это, то каждый контроллер представления, который активен в то время, когда мы переходим в фоновый режим, получает возможность внести свой вклад в пользовательскую информацию о пользовательской активности, которая появится при следующем запуске.

Реставрация

Эта часть сложнее. Информация о восстановлении поступит какsession.stateRestorationActivityв сцену делегат. Как правильно задается исходный вопрос: что теперь?

Есть несколько способов снять шкуру с этой кошки, я перепробовал большинство из них и остановился на этом. Мое правило таково:

  • Каждый контроллер представления должен иметь restorationInfoсвойство, которое является словарем. Когда любой контроллер представления создается во время восстановления, его создатель (родитель) должен установить этоrestorationInfo к userInfo который прибыл из session.stateRestorationActivity.

  • Этот userInfo должен быть скопирован в самом начале, потому что он будет удален из сохраненной активности в первый раз updateUserActivityState называется (это та часть, которая действительно сводила меня с ума при разработке этой архитектуры).

Самое интересное в том, что если мы все сделаем правильно, restorationInfoустановлен перед viewDidLoad, и поэтому контроллер представления может настраиваться на основе информации, которую он помещает в словарь при сохранении.

Каждый контроллер представления также должен удалить свой собственныйrestorationInfoкогда это будет сделано, чтобы он не использовал его снова в течение жизни приложения. Его нужно использовать только один раз при запуске.

Итак, мы должны изменить наш шаблон:

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

Итак, теперь единственная проблема - это цепочка того, как restorationInfoкаждого контроллера представления. Цепочка начинается с делегата сцены, который отвечает за установку этого свойства в корневом контроллере представления:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    guard let scene = (scene as? UIWindowScene) else { return }
    scene.userActivity =
        session.stateRestorationActivity ??
        NSUserActivity(activityType: "restoration")
    if let rvc = window?.rootViewController as? RootViewController {
        rvc.restorationInfo = scene.userActivity?.userInfo
    }
}

Каждый контроллер представления отвечает не только за настройку себя в своем viewDidLoad на основе restorationInfo, но также для того, чтобы узнать, был ли он родительским / ведущим какого-либо дополнительного контроллера представления. Если это так, он должен создать и представить / нажать / независимо от того, что контроллер представления, обязательно передатьrestorationInfo перед этим дочерним контроллером представления viewDidLoad бежит.

Если каждый контроллер представления делает это правильно, весь интерфейс и состояние будут восстановлены!

Еще немного примера

Предположим, у нас есть только два возможных контроллера представления: RootViewController и PresentViewController. Либо RootViewController представлял PresentViewController в то время, когда мы были на заднем плане, либо это не было. В любом случае, эта информация была записана в информационный словарь.

Итак, вот что делает 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)
}

Самое интересное, что PresentViewController делает то же самое!

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
}

Думаю, вы понимаете, что на данный момент это всего лишь вопрос степени. Если у нас есть больше контроллеров представлений, которые нужно связать в процессе восстановления, все они будут работать точно так же.

Заключительные примечания

Как я уже сказал, это не единственный способ снять шкуру с восстановленной кошки. Но есть проблемы со сроками и распределением ответственности, и я думаю, что это наиболее справедливый подход.

В частности, я не придерживаюсь идеи, что делегат сцены должен нести ответственность за полное восстановление интерфейса. Потребуется слишком много знать о деталях того, как инициализировать каждый контроллер представления по линии, и существуют серьезные проблемы с синхронизацией, которые трудно решить детерминированным способом. Мой подход имитирует старое восстановление на основе контроллера представления, делая каждый контроллер представления ответственным за своего потомка так же, как и обычно.

Основываясь на дополнительных исследованиях и очень полезных предложениях из ответа 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 к любым дочерним представлениям по мере необходимости в реальном приложении.

6 сентября 2019 года Apple выпустила этот образец приложения, демонстрирующий восстановление состояния iOS 13 с обратной совместимостью с iOS 12.

С Readme.md

Пример поддерживает два разных подхода к сохранению состояния. В iOS 13 и более поздних версиях приложения сохраняют состояние для каждой сцены окна с помощью объектов NSUserActivity. В iOS 12 и более ранних версиях приложения сохраняют состояние своего пользовательского интерфейса, сохраняя и восстанавливая конфигурацию контроллеров представления.

В Readme подробно рассказывается, как это работает - основной трюк заключается в том, что в iOS 12 он кодирует объект Activity (доступный в iOS 12 для другой цели) в старом encodeRestorableState метод.

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

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

А на iOS 13 он реализует недостающее автоматическое восстановление иерархии контроллера представления с помощью метода configure 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
}

Наконец, Readme включает в себя рекомендации по тестированию, но я хотел бы добавить, если вы сначала запустите симулятор Xcode 10.2, например, iPhone 8 Plus, а затем запустите Xcode 11, у вас будет iPhone 8 Plus (12.4) в качестве опции, и вы сможете испытать обратно совместимое поведение. Мне также нравится использовать эти пользовательские настройки по умолчанию, второй позволяет архиву восстановления выдерживать сбои:

[NSUserDefaults.standardUserDefaults setBool:YES forKey:@"UIStateRestorationDebugLogging"];
[NSUserDefaults.standardUserDefaults setBool:YES forKey:@"UIStateRestorationDeveloperMode"];
Другие вопросы по тегам