為了實現(xiàn)iPadOS支持多窗口类溢,Xcode11后創(chuàng)建新工程默認(rèn)會通過 UIScene 創(chuàng)建并管理多個 UIWindow 的應(yīng)用球碉,工程中除了 AppDelegate 外還會有一個 SceneDelegate矢劲。
一邀杏、SceneDelegate介紹
1)驰徊、Window與Scene
iOS13以后义辕,SceneDelegate將負(fù)責(zé)AppDelegate的某些功能虾标。 window(窗口)的概念被window(場景)的概念所代替, 一個scene現(xiàn)在可以作為您應(yīng)用程序的用戶界面和內(nèi)容的載體灌砖。iOS13以前一個應(yīng)用程序可以有不止一個window璧函,同樣現(xiàn)在一個應(yīng)用程序也可以有不止一個scene。
2)基显、SceneDelegate三處新增內(nèi)容
iOS13以后蘸吓,Xcode新建iOS項目中有增加三處新增內(nèi)容:
- 1> 添加一個新的類SceneDelegate
- 2> AppDelegate類中新增與
scene sessions
相關(guān)的新方法:
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中方法名相似, 是任何應(yīng)用程序生命周期都會調(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ù)艺谆,相當(dāng)于iOS 12上的application(_:didFinishLaunchingWithOptions:)
函數(shù)。當(dāng)將scene添加到app中時scene(_:willConnectTo:options:)
函數(shù)會被調(diào)用的拜英,因此在這里對scene進(jìn)行配置静汤。 這里需要特別注意的是,使用一個SceneDelegate來配置App中的所有scene居凶,并且這個delegate通常會響應(yīng)任何scene虫给。在上面的代碼中,我們可以手動地設(shè)置了視圖控制器堆棧侠碧,稍后會進(jìn)行詳細(xì)介紹抹估。
SceneDelegate其他方法:
-
sceneDidDisconnect(_:)
當(dāng)scene與app斷開連接是調(diào)用(注意,以后它可能被重新連接) -
sceneDidBecomeActive(_:)
當(dāng)用戶開始與scene進(jìn)行交互(例如從應(yīng)用切換器中選擇場景)時弄兜,會調(diào)用 -
sceneWillResignActive(_:)
當(dāng)用戶停止與scene交互(例如通過切換器切換到另一個場景)時調(diào)用 -
sceneWillEnterForeground(_:)
當(dāng)scene變成活動窗口時調(diào)用药蜻,即從后臺狀態(tài)變成開始或恢復(fù)狀態(tài) -
sceneDidEnterBackground(_:)
當(dāng)scene進(jìn)入后臺時調(diào)用,即該應(yīng)用已最小化但仍存活在后臺中
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ù)是負(fù)責(zé)管理Senen Session的代理函數(shù)谷暮。在應(yīng)用創(chuàng)建scene(場景)后,scene session
對象將跟蹤與該scene相關(guān)的所有信息盛垦。
這兩個函數(shù)是:
application(_:configurationForConnecting:options:)
: 方法會返回一個UISceneConfiguration對象,其中包含場景詳細(xì)信息瓤漏,包括要創(chuàng)建的場景類型腾夯,用于管理場景的代理對象以及包含要顯示的初始視圖控制器的StoryBoard。 如果未實現(xiàn)此方法蔬充,則必須在應(yīng)用程序的Info.plist文件中提供場景配置數(shù)據(jù)蝶俱。
注意:該代理方法中返回UISceneConfiguration對象的配置名為Default Configuration,則系統(tǒng)就會自動去調(diào)用SceneDelegate這個類饥漫。這樣SceneDelegate和AppDelegate產(chǎn)生了關(guān)聯(lián)榨呆。application(_:didDiscardSceneSessions:)
: 在分屏中關(guān)閉其中一個或多個scene時候回調(diào)用,可以在該函數(shù)中銷毀場景所使用的資源庸队。
該方法與application(_:didDiscardSceneSessions:)
的區(qū)別是积蜻,該方法僅在場景斷開連接時調(diào)用,不會被丟棄彻消,它可能會重新連接竿拆。而application(_: didDiscardSceneSessions:)
發(fā)生在使用應(yīng)用程序切退出場景時。
3)宾尚、Info.plist 中的Application Scene Manifest
Info.plist文件文件包含App的配置信息丙笋,如App的名稱谢澈,版本,支持的設(shè)備方向御板,現(xiàn)在我們可以通過配置Application Scene Manifest項來支持的不同場景锥忿。大多數(shù)應(yīng)用程序只有一個場景,但是可以通過配置該項創(chuàng)建更多場景怠肋,如用于響應(yīng)推送通知或特定操作的特定場景敬鬓。
Enable Multiple Windows
: 默認(rèn)為NO
,其設(shè)置為YES
可以支持多個窗口灶似。Application Session Role
: 是一個數(shù)組列林,用于在應(yīng)用程序中聲明場景。 該數(shù)組每個元素是一個字典酪惭,字典中有三個鍵值,分別為
Configuration Name
: 當(dāng)前配置的名字希痴,必須是唯一的;
Delegate Class Name
: 場景的代理類名,將與該Scene代理對象關(guān)聯(lián);
StoryBoard name
: 場景用于創(chuàng)建初始UI的storyboard名稱春感。AppDelegate方法
application(_:configurationForConnecting:options:)
返回值為UISceneConfiguration
實例砌创,上邊三個鍵值分別對應(yīng)UISceneConfiguration三個屬性name
、delegateClass
鲫懒、storyboard
嫩实。默認(rèn)在info.plist中進(jìn)行了配置, 不用
application(_:configurationForConnecting:options:)
方法也沒有關(guān)系窥岩。如果沒有在info.plist配置Application Scene Manifest項就需要實現(xiàn)這個方法并返回一個UISceneConfiguration對象甲献。
那么AppDelegate中的SceneDelegate
、UISceneSession
和Info.plist中的Application Scene Manifest
是如何一起創(chuàng)建多窗口應(yīng)用的呢颂翼?
- 首先晃洒,SceneDelegate類管理場景的生命周期,處理各種響應(yīng)朦乏,如
sceneDidBecomeActive(_:)
andsceneDidEnterBackground(_:)
之類的事件球及。 - 然后,AppDelegate類中的新函數(shù)呻疹。 它管理scene sessions(場景會話)吃引,提供場景的配置數(shù)據(jù),并響應(yīng)用戶丟棄場景的事件刽锤。
- 最后镊尺,
Application Scene Manifest
列出了當(dāng)前應(yīng)用程序支持的場景,并將它們連接到delegate類并初始化storyboard姑蓝。
三鹅心、SceneDelegate適配
從iOS13開始AppDelegate不再有window屬性,window屬性被定義在SceneDelegate中纺荧。這是因為iOS13中AppDelegate的職責(zé)發(fā)現(xiàn)了改變:
- iOS13之前旭愧,AppDelegate的職責(zé)全權(quán)處理App生命周期和UI生命周期颅筋;
- iOS13之后,AppDelegate的職責(zé)是:
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中關(guān)于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
//設(shè)置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
//設(shè)置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中的有關(guān)事件循環(huán)的方法,在iOS 13后是不會走的寓免,iOS13以下的才會收到事件回調(diào)的癣诱。iOS13以上會走SceneDelegate對應(yīng)的方法事件循環(huán)方法
func applicationWillResignActive(_ application: UIApplication) { }
...
...
四、SwiftUI中SceneDelegate
SwiftUI創(chuàng)建的iOS 13項目袜香,所以SwiftUI應(yīng)用程序主要依靠SceneDelegate來設(shè)置應(yīng)用程序的初始UI撕予。
SwiftUI項目info.plist文件中Application Scene Manifest
項配置如下:
- 默認(rèn)配置中沒有設(shè)置“Storyboard Name”這一項。但是如果要配置支持多個窗口蜈首,則需要將
Enable Multiple Windows
設(shè)置為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對象是由應(yīng)用程序創(chuàng)建的。其次荣茫,window屬性會在這里用到想帅。 App仍然使用
UIWindow
對象,但現(xiàn)在它們已成為scene(場景)的一部分计露。 在if let代碼塊中博脑,使用scene來初始化UIWindow對象。然后設(shè)置window的rootViewController票罐,將window實例賦值給場景的window屬性叉趣,并且設(shè)置窗口makeKeyAndVisible為true,即將該窗口置于App的前面该押。
接著為SwiftUI項目創(chuàng)建了ContentView實例疗杉,并通過使用UIHostingController將其添加為根視圖控制器。 該控制器用于將基于SwiftUI的視圖顯示在屏幕上蚕礼。最后烟具,UIScene的實例化對象scene實際上是UIWindowScene類型的對象。 這就是as?對可選類型轉(zhuǎn)換的原因奠蹬。 (到目前為止朝聋,已創(chuàng)建的場景通常為“ UIWindowScene”類型,但將來可能還會有更多類型的場景囤躁。)
將上邊歸納如下內(nèi)容:
- 當(dāng)
scene(_: willConnectTo: options:)
被調(diào)用時冀痕,SceneDelegate會在正確的時間配置場景。 - AppDelegate和Manifest的默認(rèn)配置狸演,他們沒有涉及storyboard的任何東西言蛇。
-
scene(_: willConnectTo: options: )
函數(shù)內(nèi),創(chuàng)建一個SwiftUI視圖宵距,將其放置在托管控制器中腊尚,然后將控制器分配給window屬性的根視圖控制器,并將該窗口放置在應(yīng)用程序UI的前面 满哪。