[iOS開發(fā)]iOS13 Scene Delegate

為了實現(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
新增SceneDelegateClass
  • 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三個屬性namedelegateClass、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(_:) and sceneDidEnterBackground(_:)之類的事件。
  • 然后,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的前面 。
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容