这有点长,但它不是微不足道的,需要很多时间来证明这个问题。
我试图弄清楚如何将一个小示例应用程序从 iOS 12 更新到 iOS 13。这个示例应用程序不使用任何情节提要(启动屏幕除外)。这是一个简单的应用程序,它显示了一个带有由计时器更新的标签的视图控制器。它使用状态恢复,因此计数器从它停止的地方开始。我希望能够支持 iOS 12 和 iOS 13。在 iOS 13 中我想更新到新的场景架构。
在 iOS 12 下,该应用程序运行良好。在全新安装时,计数器从 0 开始并上升。将应用程序置于后台,然后重新启动应用程序,计数器将从停止的位置继续。国家恢复一切正常。
现在我正在尝试使用场景在 iOS 13 下工作。我遇到的问题是找出初始化场景窗口并将导航控制器和主视图控制器恢复到场景的正确方法。
我已经阅读了尽可能多的与状态恢复和场景相关的 Apple 文档。我看过与窗口和场景相关的 WWDC 视频(212 - 在 iPad 上介绍多个 Windows,258 - 为多个 Windows 构建您的应用程序)。但我似乎错过了将它们放在一起的部分。
当我在 iOS 13 下运行应用程序时,正在调用所有预期的委托方法(AppDelegate 和 SceneDelegate)。状态恢复正在恢复导航控制器和主视图控制器,但我无法弄清楚如何设置rootViewController
场景窗口的状态,因为所有 UI 状态恢复都在 AppDelegate 中。
似乎还有一些与NSUserTask
应该使用的相关的东西,但我无法连接这些点。
缺少的部分似乎在willConnectTo
方法中SceneDelegate
。我确定我还需要stateRestorationActivity
在SceneDelegate
. 可能还需要更改AppDelegate
. 我怀疑有什么ViewController
需要改变的。
要复制我正在做的事情,请使用 Single View App 模板使用 Xcode 11(目前为 beta 4)创建一个新的 iOS 项目。将部署目标设置为 iOS 11 或 12。
删除主情节提要。删除 Info.plist 中对 Main 的两个引用(一个在顶层,一个在 Application Scene Manifest 深处。按如下方式更新 3 个 swift 文件。
AppDelegate.swift:
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("AppDelegate willFinishLaunchingWithOptions")
// This probably shouldn't be run under iOS 13?
self.window = UIWindow(frame: UIScreen.main.bounds)
return true
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
print("AppDelegate didFinishLaunchingWithOptions")
if #available(iOS 13.0, *) {
// What needs to be here?
} else {
// If the root view controller wasn't restored, create a new one from scratch
if (self.window?.rootViewController == nil) {
let vc = ViewController()
let nc = UINavigationController(rootViewController: vc)
nc.restorationIdentifier = "RootNC"
self.window?.rootViewController = nc
}
self.window?.makeKeyAndVisible()
}
return true
}
func application(_ application: UIApplication, viewControllerWithRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
print("AppDelegate viewControllerWithRestorationIdentifierPath")
// If this is for the nav controller, restore it and set it as the window's root
if identifierComponents.first == "RootNC" {
let nc = UINavigationController()
nc.restorationIdentifier = "RootNC"
self.window?.rootViewController = nc
return nc
}
return nil
}
func application(_ application: UIApplication, willEncodeRestorableStateWith coder: NSCoder) {
print("AppDelegate willEncodeRestorableStateWith")
// Trigger saving of the root view controller
coder.encode(self.window?.rootViewController, forKey: "root")
}
func application(_ application: UIApplication, didDecodeRestorableStateWith coder: NSCoder) {
print("AppDelegate didDecodeRestorableStateWith")
}
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
print("AppDelegate shouldSaveApplicationState")
return true
}
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
print("AppDelegate shouldRestoreApplicationState")
return true
}
// The following four are not called in iOS 13
func applicationWillEnterForeground(_ application: UIApplication) {
print("AppDelegate applicationWillEnterForeground")
}
func applicationDidEnterBackground(_ application: UIApplication) {
print("AppDelegate applicationDidEnterBackground")
}
func applicationDidBecomeActive(_ application: UIApplication) {
print("AppDelegate applicationDidBecomeActive")
}
func applicationWillResignActive(_ application: UIApplication) {
print("AppDelegate applicationWillResignActive")
}
// MARK: UISceneSession Lifecycle
@available(iOS 13.0, *)
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
print("AppDelegate configurationForConnecting")
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
@available(iOS 13.0, *)
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
print("AppDelegate didDiscardSceneSessions")
}
}
SceneDelegate.swift:
import UIKit
@available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
print("SceneDelegate willConnectTo")
guard let winScene = (scene as? UIWindowScene) else { return }
// Got some of this from WWDC2109 video 258
window = UIWindow(windowScene: winScene)
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
// Now what? How to connect the UI restored in the AppDelegate to this window?
} else {
// Create the initial UI if there is nothing to restore
let vc = ViewController()
let nc = UINavigationController(rootViewController: vc)
nc.restorationIdentifier = "RootNC"
self.window?.rootViewController = nc
window?.makeKeyAndVisible()
}
}
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
print("SceneDelegate stateRestorationActivity")
// What should be done here?
let activity = NSUserActivity(activityType: "What?")
activity.persistentIdentifier = "huh?"
return activity
}
func scene(_ scene: UIScene, didUpdate userActivity: NSUserActivity) {
print("SceneDelegate didUpdate")
}
func sceneDidDisconnect(_ scene: UIScene) {
print("SceneDelegate sceneDidDisconnect")
}
func sceneDidBecomeActive(_ scene: UIScene) {
print("SceneDelegate sceneDidBecomeActive")
}
func sceneWillResignActive(_ scene: UIScene) {
print("SceneDelegate sceneWillResignActive")
}
func sceneWillEnterForeground(_ scene: UIScene) {
print("SceneDelegate sceneWillEnterForeground")
}
func sceneDidEnterBackground(_ scene: UIScene) {
print("SceneDelegate sceneDidEnterBackground")
}
}
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 未连接到场景的窗口。
我错过了什么?这只是缺少一两行代码,还是我对 iOS 13 场景状态恢复的整个方法都错了?
请记住,一旦我弄清楚了下一步将支持多个窗口。因此,该解决方案应该适用于多个场景,而不仅仅是一个场景。