為了實現(xiàn)iPadOS支持多窗口,Xcode11后創(chuàng)建新工程默認會通過 UIScene 創(chuàng)建并管理多個 UIWindow 的應用,工程中除了 AppDelegate 外還會有一個 SceneDelegate。
一、SceneDelegate介紹
1)、Window與Scene
iOS13以后,SceneDelegate將負責AppDelegate的某些功能。 window(窗口)的概念被window(場景)的概念所代替, 一個scene現(xiàn)在可以作為您應用程序的用戶界面和內(nèi)容的載體。iOS13以前一個應用程序可以有不止一個window,同樣現(xiàn)在一個應用程序也可以有不止一個scene。
2)、SceneDelegate三處新增內(nèi)容
iOS13以后,Xcode新建iOS項目中有增加三處新增內(nèi)容:
- 1> 添加一個新的類SceneDelegate

- 2> AppDelegate類中新增與
scene sessions相關的新方法:
application(_:configurationForConnecting:options:)
application(_:didDiscardSceneSessions:)
- 3> Info.plist文件中新增
Application Scene Manifest配置項,用于配置App的scene,包括它們的scene配置名,delegate類名和storyboard
下面分別講解下新增三處內(nèi)容:
二、SceneDelegate三處新增內(nèi)容詳解
1)、SceneDelegate類
SceneDelegate和AppDelegate中方法名相似, 是任何應用程序生命周期都會調(diào)用方法。
//SceneDelegate.swift 代碼
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }
}
func sceneDidDisconnect(_ scene: UIScene) { }
func sceneDidBecomeActive(_ scene: UIScene) { }
func sceneWillResignActive(_ scene: UIScene) { }
func sceneWillEnterForeground(_ scene: UIScene) { }
func sceneDidEnterBackground(_ scene: UIScene) { }
}
-
scene(_:willConnectTo:options:)函數(shù) : SceneDelegate的最重要的函數(shù),相當于iOS 12上的application(_:didFinishLaunchingWithOptions:)函數(shù)。當將scene添加到app中時scene(_:willConnectTo:options:)函數(shù)會被調(diào)用的,因此在這里對scene進行配置。 這里需要特別注意的是,使用一個SceneDelegate來配置App中的所有scene,并且這個delegate通常會響應任何scene。在上面的代碼中,我們可以手動地設置了視圖控制器堆棧,稍后會進行詳細介紹。
SceneDelegate其他方法:
-
sceneDidDisconnect(_:)當scene與app斷開連接是調(diào)用(注意,以后它可能被重新連接) -
sceneDidBecomeActive(_:)當用戶開始與scene進行交互(例如從應用切換器中選擇場景)時,會調(diào)用 -
sceneWillResignActive(_:)當用戶停止與scene交互(例如通過切換器切換到另一個場景)時調(diào)用 -
sceneWillEnterForeground(_:)當scene變成活動窗口時調(diào)用,即從后臺狀態(tài)變成開始或恢復狀態(tài) -
sceneDidEnterBackground(_:)當scene進入后臺時調(diào)用,即該應用已最小化但仍存活在后臺中
2)、AppDelegate類新增兩個方法
//AppDelegate.swift 代碼
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
// MARK: UISceneSession Lifecycle
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>) {
}
}
在iOS13中AppDelegate中新增的兩個函數(shù)是負責管理Senen Session的代理函數(shù)。在應用創(chuàng)建scene(場景)后,scene session對象將跟蹤與該scene相關的所有信息。
這兩個函數(shù)是:
application(_:configurationForConnecting:options:): 方法會返回一個UISceneConfiguration對象,其中包含場景詳細信息,包括要創(chuàng)建的場景類型,用于管理場景的代理對象以及包含要顯示的初始視圖控制器的StoryBoard。 如果未實現(xiàn)此方法,則必須在應用程序的Info.plist文件中提供場景配置數(shù)據(jù)。
注意:該代理方法中返回UISceneConfiguration對象的配置名為Default Configuration,則系統(tǒng)就會自動去調(diào)用SceneDelegate這個類。這樣SceneDelegate和AppDelegate產(chǎn)生了關聯(lián)。application(_:didDiscardSceneSessions:): 在分屏中關閉其中一個或多個scene時候回調(diào)用,可以在該函數(shù)中銷毀場景所使用的資源。
該方法與application(_:didDiscardSceneSessions:)的區(qū)別是,該方法僅在場景斷開連接時調(diào)用,不會被丟棄,它可能會重新連接。而application(_: didDiscardSceneSessions:)發(fā)生在使用應用程序切退出場景時。
3)、Info.plist 中的Application Scene Manifest

Info.plist文件文件包含App的配置信息,如App的名稱,版本,支持的設備方向,現(xiàn)在我們可以通過配置Application Scene Manifest項來支持的不同場景。大多數(shù)應用程序只有一個場景,但是可以通過配置該項創(chuàng)建更多場景,如用于響應推送通知或特定操作的特定場景。
Enable Multiple Windows: 默認為NO,其設置為YES可以支持多個窗口。Application Session Role: 是一個數(shù)組,用于在應用程序中聲明場景。 該數(shù)組每個元素是一個字典,字典中有三個鍵值,分別為
Configuration Name: 當前配置的名字,必須是唯一的;
Delegate Class Name: 場景的代理類名,將與該Scene代理對象關聯(lián);
StoryBoard name: 場景用于創(chuàng)建初始UI的storyboard名稱。AppDelegate方法
application(_:configurationForConnecting:options:)返回值為UISceneConfiguration實例,上邊三個鍵值分別對應UISceneConfiguration三個屬性name、delegateClass、storyboard。默認在info.plist中進行了配置, 不用
application(_:configurationForConnecting:options:)方法也沒有關系。如果沒有在info.plist配置Application Scene Manifest項就需要實現(xiàn)這個方法并返回一個UISceneConfiguration對象。
那么AppDelegate中的SceneDelegate、UISceneSession和Info.plist中的Application Scene Manifest是如何一起創(chuàng)建多窗口應用的呢?
- 首先,SceneDelegate類管理場景的生命周期,處理各種響應,如
sceneDidBecomeActive(_:)andsceneDidEnterBackground(_:)之類的事件。 - 然后,AppDelegate類中的新函數(shù)。 它管理scene sessions(場景會話),提供場景的配置數(shù)據(jù),并響應用戶丟棄場景的事件。
- 最后,
Application Scene Manifest列出了當前應用程序支持的場景,并將它們連接到delegate類并初始化storyboard。
三、SceneDelegate適配
從iOS13開始AppDelegate不再有window屬性,window屬性被定義在SceneDelegate中。這是因為iOS13中AppDelegate的職責發(fā)現(xiàn)了改變:
- iOS13之前,AppDelegate的職責全權處理App生命周期和UI生命周期;
- iOS13之后,AppDelegate的職責是:
1、處理 App 生命周期
2、新的 Scene Session 生命周期
3、UI的生命周期交給新增的Scene Delegate處理。
因此,iOS13以前創(chuàng)建項目如果不需要多窗口就不需要任何改動,而iOS13以后創(chuàng)建新項目時,就要做一些適配:
1. 不需要多窗口(multiple windows)
- 刪除掉info.plist中Application Scene Manifest選項,同時,注釋SceneDelegate文件中所有代碼,SceneDelegate文件刪不刪除都可以。
- 注釋 AppDelegate中關于Scene的代理方法
如果使用純代碼來實現(xiàn)顯示界面,需要在AppDelegate.h中手動添加window屬性,添加以下代碼即可:
class AppDelegate: UIResponder, UIApplicationDelegate {
//手動添加window屬性
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame:UIScreen.main.bounds)
self.window!.backgroundColor = UIColor.white
//設置root
let rootVC = UIViewController()
self.window!.rootViewController = rootVC
self.window!.makeKeyAndVisible()
return true
}
// MARK: UISceneSession Lifecycle
// 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>) {
// }
}
2. 支持多窗口適配
iOS 13后新項目中info.plist中的配置項Application Scene Manifest是針對iPad multiple windows功能推出的。在保留Application Scene Manifest配置項不予刪除時(其中,項目是否支持多窗口功能是個可勾選項),AppDelegate的生命周期方法不再起作用,需要在SceneDelegate中使用UIScene提供的生命周期方法,并且需要針對 iOS 13 在Scene中配置和 iOS 13 以下在AppDelegate中做兩套配置。
下面是純代碼實現(xiàn)界面顯示的代碼:
Swift適配代碼步驟:
- 1)第一步,SceneDelegate中添加@available(iOS 13, *)
//SceneDelegate.swift
@available(iOS 13, *) //在類的頭部@available(iOS 13, *)添加即可
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
....
....
}
- 2)第二步,AppDelegate中聲明window屬性,
didFinishLaunchingWithOptions中添加版本判斷,AppDelegate中新增兩個方法前添加@available(iOS 13, *)。也可以將這兩個方法添加到AppDelegate分類中,分類前添加@available(iOS 13, *)。
// AppDelegate.swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
//手動添加window屬性
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if #available(iOS 13, *) {
} else {
window = UIWindow(frame:UIScreen.main.bounds)
window!.backgroundColor = UIColor.blue
//設置root
let rootVC = UIViewController()
window!.rootViewController = rootVC
window!.makeKeyAndVisible()
}
return true
}
//新增方法添加@available(iOS 13, *)
@available(iOS 13.0, *)
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
@available(iOS 13.0, *)
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
}
}
- 3)第三步,SceneDelegate中初始化UIWindow,并添加根視圖控制器
@available(iOS 13, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
let vc = ViewController()
vc.view.backgroundColor = .red
let navigation = UINavigationController(rootViewController: vc)
window.rootViewController = navigation
window.makeKeyAndVisible()
self.window = window
}
...
...
}
OC適配代碼:
// AppDelegate.m中
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
if (@available(iOS 13.0, *)) {
} else {
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
[self.window setBackgroundColor:[UIColor whiteColor]];
ViewController *vc = [[ViewController alloc] init];
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
[self.window setRootViewController:nav];
[self.window makeKeyAndVisible];
}
return YES;
}
// SceneDelegate.m中
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
//在這里手動創(chuàng)建新的window
if (@available(iOS 13.0, *)) {
UIWindowScene *windowScene = (UIWindowScene *)scene;
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
[self.window setWindowScene:windowScene];
[self.window setBackgroundColor:[UIColor whiteColor]];
ViewController *con = [[ViewController alloc] init];
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:con];
[self.window setRootViewController:nav];
[self.window makeKeyAndVisible];
}
}
- 注意:如果不使用storyboard,需要將配置中的storyboard項刪除
- 注意2:AppDelegate中的有關事件循環(huán)的方法,在iOS 13后是不會走的,iOS13以下的才會收到事件回調(diào)的。iOS13以上會走SceneDelegate對應的方法事件循環(huán)方法
func applicationWillResignActive(_ application: UIApplication) { }
...
...
四、SwiftUI中SceneDelegate
SwiftUI創(chuàng)建的iOS 13項目,所以SwiftUI應用程序主要依靠SceneDelegate來設置應用程序的初始UI。
SwiftUI項目info.plist文件中Application Scene Manifest項配置如下:

- 默認配置中沒有設置“Storyboard Name”這一項。但是如果要配置支持多個窗口,則需要將
Enable Multiple Windows設置為YES。 - AppDelegate類,和上邊iOS新建項目AppDelegate一樣。
- SceneDelegate類中實現(xiàn)代碼,如下
//SceneDelegate.swift
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
.....
.....
}
上面的代碼中發(fā)生了什么?
使用此方法可以有選擇地配置UIWindow窗口并將其附加到提供的UIWindowScene場景。
如果使用storyboard,則window屬性將自動初始化并附加到場景中。
首先,添加新場景會調(diào)用
scene(_: willConnectTo: options:)方法。 方法傳入一個scene對象和一個session,傳入的scene對象是由應用程序創(chuàng)建的。其次,window屬性會在這里用到。 App仍然使用
UIWindow對象,但現(xiàn)在它們已成為scene(場景)的一部分。 在if let代碼塊中,使用scene來初始化UIWindow對象。然后設置window的rootViewController,將window實例賦值給場景的window屬性,并且設置窗口makeKeyAndVisible為true,即將該窗口置于App的前面。
接著為SwiftUI項目創(chuàng)建了ContentView實例,并通過使用UIHostingController將其添加為根視圖控制器。 該控制器用于將基于SwiftUI的視圖顯示在屏幕上。最后,UIScene的實例化對象scene實際上是UIWindowScene類型的對象。 這就是as?對可選類型轉(zhuǎn)換的原因。 (到目前為止,已創(chuàng)建的場景通常為“ UIWindowScene”類型,但將來可能還會有更多類型的場景。)
將上邊歸納如下內(nèi)容:
- 當
scene(_: willConnectTo: options:)被調(diào)用時,SceneDelegate會在正確的時間配置場景。 - AppDelegate和Manifest的默認配置,他們沒有涉及storyboard的任何東西。
-
scene(_: willConnectTo: options: )函數(shù)內(nèi),創(chuàng)建一個SwiftUI視圖,將其放置在托管控制器中,然后將控制器分配給window屬性的根視圖控制器,并將該窗口放置在應用程序UI的前面 。