iOS 13 Xcode11 中的 Scene Delegate

如果將Xcode更新到11, 創建項目。默認會創建SceneDelegate.swift, 那麼問題來了, 這個代理用來幹嘛的了?

在這篇文章中,我們將探索iOS13和Xcode11的改變。我們着重介紹scene和Delegates , 看看他們事如何影響SwiftUI,Storyboard和Xib構建的UI.

我們將學習到:

  • AppDelegate和SceneDelegate
  • 程序啓動時,他們如何一起工作的
  • 怎麼設置app的scene deleagate
  • 在Storyboard和SwiftUI不同環境中如何使用scene delegate

繫好安全帶,發車。。。

這篇文檔項目環境Xcode11 和 iOS13

AppDelegate

我們對於AppDelegate非常熟悉,他是一個App啓動的入口,其中的 application(_:didFinishLaunchingWithOptins: ) 方法是系統操作喚醒的第一個方法。

AppDelegate 遵守UIKit框架的UIApplicationDeleaget ,但是在iOS13中app delegate發生了改變 ,我們能很快發現。

以下是iOS12 app deleget 的常規操作:

  • 設置第一個ViewController,我們叫做它root View Controller吧
  • 配置app設置和啓動模塊,比如登錄,連接服務器等
  • 註冊推送通知回調,響應發送到app的推送通知
  • 響應app生命週期事件,比如進入後臺,喚醒和退出應用

使用Storyboard啓動的app, app delegate都是千篇一律的。因爲它只返回true 就像下面這個方法:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
    return true
}

一個簡單的使用Xib的app, 需要設置自己的根控制器,就像這樣:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{   
    let timeline = TimelineViewController()
    let navigation = UINavigationController(rootViewController: timeline)

    let frame = UIScreen.main.bounds
    window = UIWindow(frame: frame)

    window!.rootViewController = navigation
    window!.makeKeyAndVisible()

    return true
}

在上面的代碼中,我們創建了一個控制器, 並且把他放在導航欄控制器中,把他分配爲給 UIWindow對象的rootController屬性。 這個window對象是 app delegate的屬性, 是我們app擁有的一個window.

app的window是iOS中一個非常重要的概念, 一般一個window就是一個app,而且大部分iOS 應用只有一個window, 它就是你應用 UI界面的可視, 用戶點擊事件, 提供後臺顯示內容。 當然這裏的window和微軟的Windows不是一個東西, 是不同的概念(謝謝你 Xerox!)。

好現在我們把重點放在scene delegate。

SceneDelegate

在iOS 13及以上 scene delegate 將扮演 一些 app delegate 的作用, 大多數情況窗口(window)的概念被場景(scene)替換。 一個應用可以有多個場景, 而場景又是作爲應用的界面和內容的呈放。

一個app有多個場景的概念來說是特別地, 而且這樣就允許您創建多窗口的iOS或者iPadOS應用。在一個 文本處理app中,每一個文檔都可以有自己的場景。例如, 用戶可以創建一個場景的複製場景, 一次可以運行一個app的多實例。

在Xcode 11中跳轉使用scene delegate有三個地方:

  1. 新建一個項目,會自動創建SceneDelegate 類, 它包括 生命週期事件,比如 動作, 註冊, 連接等
  2. AppDelegate中有兩個關聯scene sessions的方法,叫做 application(_:configurationForConnecting: options:) 和 application(_: didDiscardSceneSessions:)
  3. 在 Info.plist文件中的屬性列表中有一個Application Scene Manifests(應用場景清單)屬性(如下圖),可以進行相應配置, 比如類、代理、 storyboard名字等

好,讓我們一步一步操作。

1. 場景代理類(Scene Delegate Class)

SceneDelegate類如:

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?


    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
        guard let _ = (scene as? UIWindowScene) else { return }
    }

    func sceneDidDisconnect(_ scene: UIScene) {
        // Called as the scene is being released by the system.
        // This occurs shortly after the scene enters the background, or when its session is discarded.
        // Release any resources associated with this scene that can be re-created the next time the scene connects.
        // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
    }

    func sceneDidBecomeActive(_ scene: UIScene) {
        // Called when the scene has moved from an inactive state to an active state.
        // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
    }

    func sceneWillResignActive(_ scene: UIScene) {
        // Called when the scene will move from an active state to an inactive state.
        // This may occur due to temporary interruptions (ex. an incoming phone call).
    }

    func sceneWillEnterForeground(_ scene: UIScene) {
        // Called as the scene transitions from the background to the foreground.
        // Use this method to undo the changes made on entering the background.
    }

    func sceneDidEnterBackground(_ scene: UIScene) {
        // Called as the scene transitions from the foreground to the background.
        // Use this method to save data, release shared resources, and store enough scene-specific state information
        // to restore the scene back to its current state.
    }


}

場景代理中最重要的方法是 scene(_:willConnectTo:options:), 一般來說它等同於iOS 13之前的 application(_: didFinishLaunchingWithOptions:)方法, 場景被添加到app,這個方法將被調用, 這是配置場景完美的地方。 在上面的代碼中, 我們可以設置controller棧,這個設置我們等會兒再說

重點注意SceneDelegate在使用delegate時,當然 一個delegate 也可以響應任何場景,如果你想使用一個代理配置所有場景的話。

SceneDelegate也包含下面這些方法:

  • sceneDidDisConnect(_:) 場景沒有連接時調用(等一會兒他會再連接)
  • sceneDidBecomeActive(_:) 當用戶開始進入場景時調用,比如從app switcher(應用切換)選中app
  • scenewillResignActive(_:) 當用戶停止進入場景,比如進入另一個場景
  • sceneWillEnterForeground(_:) 當用戶將要進入前臺,比如從後臺開始或者喚醒
  • sceneDidEnterBackground(_:) 當場景將要進入後臺,比如 app 最小化,但是依然在後臺運行  

 

2. 應用代理:場景會話(AppDelegate : Scene Sessions)

在iOS13中, Appdelegate類現在包含了與secene sessions 相關的方法。 創建一個場景的同是 場景對象也會隨之創建,場景會話對象跟蹤管理場景的相關信息, 方法如下:

  • application(_: configurationForConnecting: optings:) 這個是返回場景配置對象的。
  • application(_:didDiscardSceneSessions:) 你的app用戶關閉一個或者多個場景時調用。

現在, 場景會話用於指定一個場景, 比如 "External Display" 或者 "CardPlay", 也被用來存儲一個場景的狀態, 用於狀態恢復非常好用。 狀態恢復允許你恢復或者再次創建UI在app啓動期間。你也可以分配用戶信息到場景會話中, 這是一個你可以放入任何能放入的字典。

application(_:didDiscardSceneSession:) 是非常簡單的, 當用戶通過應用切換來關閉一個或者多個場景時會調用。你可以在這裏釋放一些你場景使用的資源,因爲他們不在需要使用了。

對比與 sceneDidDisconnect(_:) , 這個方法標識當場景僅僅失去連接,但是不一定丟棄。 它可能會嘗試重連, 直到application(_:didDiscardSceneSession:) 使用應用切換標記場景退出。

3.Info.plist中的應用場景清單(Application Scene Manifest)  

你使用的每一個場景都需要在應用場景清單中聲明過, 簡而言之就是清單列出你應用支持的場景。 大多是app只有一個場景,但是你能創建更多個,比如響應通知或者事件的單獨場景。

應用場景清單也是Info.plist文件中的一部分,Info.plist也是一個明確知道應用配置的好地方。該文件屬性列表包含應用名字,版本號,支持設備方向等

注意在這裏聲明session的類型, 而不是session本身。 你的app能支持一個場景, 創建一個場景的副本和使用它來創建多窗口app.

以下是Info.plist的列表大概:

在頂層,你可以看見應用場景清單子項,下面時Enable Multiple Windows ,如果你需要支持多窗口,需要設置爲YES. 再往下是一個聲明應用內場景的Applicaiton Session Role數組。 另一個部分能用於聲明外部場景。

最重要的信息是Application Session Role 數組部分, 包括:

  • Configeration Name 配置的名字,必備的
  • 場景的類名, UIWindowScene
  • Delegate Class Name 場景代理類名,一般是SceneDelegate
  • 場景初始化storyboard的名字

storyboard的名字部分主要提醒用戶主Storyboard,以便於在Xcode 12 的項目屬性配置中看到。 一般基本iOS 應用中, 如果你不使用場景這就是你設置或改變主要Storyboard的地方【譯者發現,如果需要改變主顯示Storyboard,只能在這裏修改,直接在項目的General -> Deployment Info -> Main Interface中修改是沒有效果的。 】。

如何用SceneDelegate、 AppDelegate中的scene session 應用場景清單怎麼創建多窗口app?

  • 首先,我們已經瞭解過Scenedelegate類, 它管理場景的聲明週期,響應事件(比如sceneDidBecomeActive(_:)和 sceneDidEnterBackground(_:))
  • 之後,我們檢查Appdelegate的新方法,它管理場景會話,提供場景配置數據, 用戶丟棄場景響應
  • 最後,我們查看應用場景清單(Application Scene Manifest),它羅列出你應用支持的場景, 代理類和初始化storyboard

太棒了!有了初步瞭解,我們看看用Xcode11創建UI,看看是如何影響的。

基於SwiftUI的Scene Delegate

iOS13最簡單創建項目方法是使用SwiftUI, 簡單來說  SwfitUI創建的項目大多是都是通過SceneDelegate設置初始化UI的

首先看看SwiftUI的應用場景清單長什麼樣子:

這是非常標準的,對於默認app,哪裏標準了? 使用Default Configuration標識不包含的Storyboard Name Set 。記住, 如果你想支持多窗口,需要吧Enable Multiple Windows 設置爲 YES.

我們跳過AppDelegate, 因爲它非常標準,只返回true.

下一步, 看看 SceneDelegate類, 在我們修改之前,場景代理設置了你應用的場景響應。和設置她們的初始化視圖。

就像這樣:

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()
        }
    }

上面的代碼做了什麼了?

首先,認真考慮scene(_:willConnetectTo:options:)協議方法(當一個新場景加入),提供scene對象和session, 這個UIWindowScene對象是app創建的,如此一來你就不需要手動創建。

之後,這個window屬性被使用,應用仍然有UIWindow的window對象,只不過現在他們是場景的一部分。在代碼中,在if let 閉包內,你可以清楚的看見使用scene參數初始化的UIWindow實例。

設置window的rootViewController屬性,然後這個window變爲可見(make key add visible),目的是把它放在UI層的最前面。

使用SwiftUI,你要注意,被創建的ContentView作爲root view controller 通過使用UIHostingController, 這個控制器把SwiftUI基本視圖放在屏幕上。

方法中有一點注意,類型爲UIScene的scene參數,實際上UIWindowScene類型的實例。使用as 可選解析(到目前爲止,創建的場景通常是UIWindowScene類型,但我猜想在未來,我們會看到更多類型的場景。)

所有的看起來很複雜,但是總體來看非常簡單。

  • scene delegate 中配置場景,合適的時間,就是當scene(_:willConnectTo:options:) 被調用時。
  • app delegate 和Manifest ,有默認的配置,不需要引入storyboard
  • scene(_:willConnectTo:options:)方法中創建SwiftUI的視圖, 放在hosting controller中,分配它爲window屬性的root view controller,把他們設置爲UI層最前端就好了。

非常棒!讓我們開始

你可以使用Xcode11創建基本項目, 選擇SwfitUI, 通過選擇File -> New -> Project, 之後選擇 Single View App, 最後選擇 SwiftUI作爲User Interface

 

基於Storyboard的SceneDelegate

Storyboards, XIBs, 是創建UI的有效的方式。 但是在iOS 13上是一樣的。在以後,我們會看到越來越多SwiftUI 應用, 但是現在, storyboard更常用。

有趣的是,你無法多餘操作,只需要選擇File-> New -> Project, 選擇Single View App, 最後選擇 Storyboard作爲User Interface , 就完成了。

下面使步驟:

  • 就像之前所述,在Info.plist,你發現Main storyboard 在 Application Scene Manifest的字典中
  • 默認app delegate將使用默認的場景配置
  • 默認scene delegate  設置 UIWindow對像,使用Main.storyboard創建初始化UI

 

設置你應用的編程方式

很開發者使用代碼創建UI, 隨着SwiftUI的崛起, 我們能看到更多。 那如果你不是用Storyboard,而是使用xib來創建你得app UI , 來看看場景代理如何適配它吧。

首先, app delegate 和 Applicaton Scene Manifest 使完整的, 默認設置的。 我們沒有使用storyboard,我們要在 SceneDelegate 的 scene(_:willConnectTo: options:)方法內設置初始化控制器。

就像這樣:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)
    {
        if let windowScene = scene as? UIWindowScene {

            let window = UIWindow(windowScene: windowScene)
            let timeline = TimelineViewController()

            let navigation = UINavigationController(rootViewController: timeline)
            window.rootViewController = navigation

            self.window = window
            window.makeKeyAndVisible()
        }
    }

 我們來看看發生了啥:

  • 就像之前,我們持有UIWindow類型的window屬性,它是用windowScene對象初始化的,該對象是由scene參數進行類型轉換的
  • 在 if le閉包內, 就像上面的代碼, 這就是在iOS 12及之前設置根控制器的方式在AppDelegate, 你廚師化一個view controller, 把他放在導航欄控制器裏, 設置給rootViewController
  • 最後,window常量分配給window屬性, 然後讓它可見,把他放在屏幕最前面。 

很簡單,對吧? 最核心的是,把你之前在app delegate中代碼移到scene delegate ,配置Applicaton Scene Manifest

還在找怎麼爲已存在的項目添加場景支持嗎? 看這個吧

 

深入學習

完美了!這只是示例的一小部分, 隨着我們的深入,場景代理允許你添加多窗口應用

你可能已經學會怎樣爲SwiftUI、Storyboard設置場景代理, 我們也看了使場景工作的三部分:app delegate , scene delegate , Application Scene Manifest , 非常棒!

想要學習更多嗎? 通過下面資源學習吧:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章