【发布时间】:2019-11-29 11:08:35
【问题描述】:
这有点长,但也不是小事,证明这个问题需要很多时间。
我试图弄清楚如何将一个小示例应用从 iOS 12 更新到 iOS 13。这个示例应用不使用任何情节提要(启动屏幕除外)。这是一个简单的应用程序,它显示了一个带有由计时器更新的标签的视图控制器。它使用状态恢复,因此计数器从它停止的地方开始。我希望能够支持 iOS 12 和 iOS 13。在 iOS 13 中我想更新到新的场景架构。
在 iOS 12 下,该应用运行良好。在全新安装时,计数器从 0 开始并上升。将应用程序置于后台,然后重新启动应用程序,计数器将从停止的位置继续。状态恢复一切正常。
现在我正在尝试使用场景在 iOS 13 下运行。我遇到的问题是找出初始化场景窗口并将导航控制器和主视图控制器恢复到场景的正确方法。
我已经阅读了尽可能多的与状态恢复和场景相关的 Apple 文档。我看过与窗口和场景相关的 WWDC 视频(212 - Introducing Multiple Windows on iPad,258 - Architecting Your App for Multiple Windows)。但我似乎缺少将它们组合在一起的部分。
当我在 iOS 13 下运行应用程序时,所有预期的委托方法(AppDelegate 和 SceneDelegate)都被调用。状态恢复是恢复导航控制器和主视图控制器,但我不知道如何设置场景窗口的rootViewController,因为所有的 UI 状态恢复都在 AppDelegate 中。
似乎还有一些与应该使用的 NSUserTask 相关的东西,但我无法连接这些点。
缺失的部分似乎在SceneDelegate 的willConnectTo 方法中。我确定我还需要对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 场景状态恢复的整个方法都错了?
请记住,一旦我明白这一点,下一步将支持多个窗口。因此,该解决方案应该适用于多个场景,而不仅仅是一个场景。
【问题讨论】:
-
能否请您编辑问题以澄清您的应用是适用于 iPhone、iPad 还是通用的?
-
@malhal 这与问题无关。为什么这很重要?
-
场景是可选的,因此如果它仅适用于 iPhone,则不要并继续正常使用状态恢复。场景甚至不应该在 iOS 中,而应该在 iPadOS 中。
-
@malhal 关于仅限 iPhone 的应用程序的要点。所以假设这是关于 iPad 或通用应用程序的。
标签: ios ios13 state-restoration uiscene