Scene Delegate
为了实现多窗口功能,苹果修改了使用多年的AppDelegate。在Xcode 11中新建工程时,会发现工程文件中包含AppDelegate.swift,SceneDelegate.swift。多出来的SceneDelegate.swift就是本文的基础内容。
回忆一下在iOS 13之前我们是怎么使用AppDelegate的?
在didFinishLaunchingWithOptions启动入口方法中,进行初始化数据库,启动必要的服务,注册通知等基础工作,然后创建UIWindow,执行UIWindow的makeKeyAndVisible方法,即将页面显示在屏幕上。
除了didFinishLaunchingWithOptions启动入口方法,UIApplicationDelegate还提供有一系列的方法以供管理APP的生命周期:
func applicationWillResignActive(_ application: UIApplication) {
//即将变为非活动状态
func applicationDidEnterBackground(_ application: UIApplication) {
//已进入后台
func applicationWillEnterForeground(_ application: UIApplication) {
//即将进入前台
func applicationDidBecomeActive(_ application: UIApplication) {
//已进入前台,成为活跃进程
func applicationWillTerminate(_ application: UIApplication) {
//进程终止
从iOS 13开始,这几个从iOS 2.0起沿用至今的方法将有所变化:原有的UIApplicationDelegate UI生命周期的几个方法将拆分到UISceneDelegate中:
// 新建窗口时调用
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)
// 通过app switcher关闭该窗口时调用,至此窗口生命结束。
func sceneDidDisconnect(_ scene: UIScene)
func sceneDidBecomeActive(_ scene: UIScene)
func sceneWillResignActive(_ scene: UIScene)
func sceneWillEnterForeground(_ scene: UIScene)
func sceneDidEnterBackground(_ scene: UIScene)
相应的AppDelegate中新增对scene的支持:
//新建场景,返回场景配置
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
//场景关闭时调用
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
UIApplicationDelegate提供的applicationWillTerminate接口保持不变,即UIApplicationDelegate不再负责UI的生命周期,仅负责APP的生命周期,包括启动和终止。其他四个方法分别替换为UISceneDelegate提供的sceneWillResignActive等,UISceneDelegate额外新增willConnectTo,sceneDidDisconnect接口以控制场景的新建和关闭。
即iOS 13中引入了scene的概念,每一个scene对应一个UIWindow,UIWindow由UISceneDelegate管理。iOS 13之前一个APP也是可以创建多个UIWindow的,但只能有一个keyWindow。引入scene之后,每一个scene都会有一个keyWindow。
Info.plist:Application Scene Manifest
APP支持的场景需要在Info.plist中声明,由Info.plist->Application Scene Manifest->Scene Configuration->Application Session Role节点指定场景List,每一项包含以下节点:
Configuration Name:场景配置的唯一标识;
Delegate Class Name:实现UIWindowSceneDelegate代理类的名称;
Storyboard Name:Storyboard的名称(如果采用的是Storyboard方式实现UI),可选。
一种类型的场景对应一个Scene Configuration。以iPadOS 13原生APP为例,Safari支持多窗口,但它的每一个窗口都是一样的(一个场景),都是一个web浏览器;而Mail则支持多窗口也有多个窗口类型(邮件列表和写邮件窗口是两个不同的场景)。当然,你完全可以定义两个Configuration Name不同,但页面一样的窗口,从技术层面这样没问题,只是不符合Apple的设计规范。
iOS中适配Scene Delegate
使用Scene Delegate之后,UIApplicationDelegate将不再持有UIWindow,它将转移至UIWindowSceneDelegate代理中。由于iOS目前还不支持多窗口,所以大部分情况下iOS项目仅需要一个场景配置。典型的改造方式如下:
didFinishLaunchingWithOptions
AppDelegate中,didFinishLaunchingWithOptions仅负责处理除UI之外的初始化工作:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// init database,register notification,and so on.
return true
configurationForConnecting
iOS 13系统将调用configurationForConnecting接口,返回UISceneConfiguration对象,创建scene。
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
if let activity = options.userActivities.first, activity.activityType == "com.kanchuan.scene0" {
return UISceneConfiguration(name: "scene0", sessionRole: connectingSceneSession.role)
return UISceneConfiguration(name: "scene1", sessionRole: connectingSceneSession.role)
UISceneSession是管理scene的实例,一个scene对应一个UISceneSession,UISceneSession包含唯一标识和场景的配置细节。UISceneSession的生命周期由UIKit维护,当scene关闭时UISceneSession销毁。开发者无法直接创建UISceneSession,除了在configurationForConnecting接口由系统传入UISceneSession的实例外,开发者也可以通过UIApplication.shared.requestSceneSessionActivation的方式动态创建新的场景。
UIScene.ConnectionOptions则提供了创建场景时携带的额外信息,以便启动不同的场景配置。
willConnectTo
接着系统将在场景配置list中找到Configuration Name(scene0)指定的Delegate Class Name(Scene0Delegate),控制权交由Scene Delegate,将执行willConnectTo方法,在这个方法中完成创建UIWindow的操作。
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = ViewController()
window.backgroundColor = .white
self.window = window
window.makeKeyAndVisible()
之后,页面进入前后台的事件全由Scene Delegate接管。在上述示例中,configurationForConnecting方法中有区分是走scene0场景还是scene1场景,options的参数是调用时传递的:当直接打开APP时,options中数据为空;通过UIApplication.shared.requestSceneSessionActivation方式切换场景时,options将包含携带的用户数据。
iOS各个版本的适配
由于Scene Delegate仅在iOS 13获得支持,iOS 13之前的系统还是需要走UIApplicationDelegate相关的方法。两个方案可供适配:
不支持Scene Delegate:直接删除Info.plist的Application Scene Manifest节点,回到使用UIApplicationDelegate的方式;
支持Scene Delegate:通过#available(iOS 13.0, *)等方式区分版本,iOS 13之前系统走原始的UIApplicationDelegate,iOS 13之后走Scene Delegate。
iPadOS 多窗口
自iOS 13开始,苹果推出了iPad专用的iPadOS系统,表明了苹果将iPad打造成生产力工具的野心。期待已久的multiple windows功能终于在iPadOS 13上得以实现。
上面讲到的iOS适配Scene Delegate是非常简单的,就是将UIWindow的创建及UI生命周期管理放置在Scene Delegate中,其它的比如页面栈管理等和之前完全一样。Scene Delegate的更大的用途是用来支持iPadOS的多窗口。
想要iPad App获得多窗口的能力非常简单,在Xcode项目中勾选“Supports multiple windows”即可,运行打开APP->选中APP长按->Show All Windows->点击屏幕右上角+号,即可新建一个窗口。
就是这么简单,你甚至不需要添加任何代码就可实现多窗口的能力。但我们的需求肯定不止于此,我们还需要在代码中控制窗口的新建,关闭以及用户数据交互。苹果提供了简洁的接口方便我们编程控制。
新建scene
let activity = NSUserActivity(activityType: "com.kanchuan.scene0")
activity.userInfo = ["website": "https://kanchuan.com/blog"]
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil, errorHandler: nil)
为了达到各个场景之间切换时状态保留的目的,用户数据的传递就变得非常关键。苹果使用NSUserActivity(iOS 8加入的,很多地方都用到了)来存储用户数据。NSUserActivity亦可从UISceneSession的stateRestorationActivity方法获取,典型的使用场景如下:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let vc: ViewController
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity,
let identifier = activity.targetContentIdentifier {
vc = ViewController(catName: identifier)
} else {
vc = ViewController(catName: "default")
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = vc
window.backgroundColor = .white
self.window = window
window.makeKeyAndVisible()
这样就可以在载入场景时获得暂存的用户数据,以便UI平滑过渡。关于NSUserActivity的内容可以参考文档。
关闭scene
if let session = self.view.window?.windowScene?.session {
let options = UIWindowSceneDestructionRequestOptions()
options.windowDismissalAnimation = .commit
UIApplication.shared.requestSceneSessionDestruction(session, options: options, errorHandler: nil)
上面讲解了Scene Delegate和multiple windows的一般知识,更酷的高级用法比如拖拽多窗口交互等后续有时间精力在研究。
WWDC2019 258
Understanding the iOS 13 Scene Delegate
文章最后修改于 2023-08-01
相关文章:
Xcode 10/iOS 12适配
iOS NSAttributedString NSHTMLTextDocumentType陷阱
iOS安全:使用dumpdecrypted/Clutch 砸壳
iOS 14 适配:更严格的用户隐私保护
xcodebuild build failed:Use the $(inherited) flag