如何在使用SceneDelegate的專案中實現小窗需求?
前言
之前做過一個直播間的小窗需求,在使用者進入到其它頁面的時候,依然可以觀看直播。而諸如bilibili的視訊,微信的視訊號,網易雲音樂的廣場等手機端視訊小窗,在iOS 14釋出之後,都使用了Apple官方的畫中畫功能來實現小窗播放。
具體表現如上,可以具備很多Apple提供的畫中畫的功能,注意該小窗可以在應用內,也可以在應用外:
- 雙擊小窗:改變尺寸,變大變小
- 拖動小窗:改變小窗的位置
- 向左或右邊緣拖動:隱藏小窗
- 點選左上角關閉:關閉小窗
- 點選右上角迴歸:返回App並全屏觀看
說了這麼多優點,那麼缺點我想顯而易見了,必須使用Apple提供的*AVPlayerViewController
* 或者**AVPictureInPictureController
這兩種系統控制器來實現小窗需求,那麼不可避免的會造成可定製性就會比較低!所以在BILIBILI的最新版本(7.1.2)中它們沒有在應用內使用Apple的畫中畫功能,只是應用外使用了,那麼應用內如果不使用Apple的畫中畫特性,那麼如何實現小窗播放呢?
答案顯而易見:UIWindow。
AppDelegate和SceneDelegate的關聯
其實在提到這個UIWindow的建立的時候,有必要去提一提SceneDelegate出來之後的一些變化。在iOS 13之前的App,AppDelegate是App主要的入口,並且是App的各種不同狀態切換處理的地方。但是在iOS 13之後,原來AppDelegate的職責就被劃分為AppDelegate和SceneDelegate共同承擔了,主要的原因是要滿足iPad-OS中支援的多視窗的特性。
那麼現在它們的職責分別是什麼呢?
AppDelegate
職責
依然是整個應用的入口,負責整個App級別的生命週期以及啟動設定。
方法
在iOS 13之後,目前AppDelegate預設會有三個方法,分別如下:
swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
- 此方法用於整個應用的啟動,以及初始化的設定。
swift
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration
- 當一個新的Scene被建立時該方法被呼叫,在啟動時並不會呼叫該方法。
swift
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>)
- 當用戶從多視窗中移除該Scene或者使用程式銷燬該Scene時,該方法被呼叫。
其它的關於整個App生命週期的方法,以及定位,推送等相關方法這裡不做贅述。
SceneDelegate
職責
原來window的概念現在被scene所取代,一個App可以有很多個不同的Scene,而Scene現在作為App的使用者介面和內容的管理,同時一個Scene上又可以有很多的UIWindow(本質上UIWindow是UIView)。所以SceneDelegate的職責是管理App中的UI的生命週期(也就是管理Scene的生命週期)。
關於Scene的理解如果接觸過Unity遊戲開發應該會很容易,在遊戲中不同的關卡其實就是不同的場景(Scene),而同一個場景中可以許多不同的視窗檢視(UIWindow)。而切換不同的關卡,就是不同場景的切換。所以如果一個App如果要承載業務上許多不同端的功能(如管理端,消費端),其實可以使用不同的Scene來進行這個切換。
方法
整體來說SceneDelegate和iOS 13以前的AppDelegate的方法含義類似,一看就知道是關於各種狀態管理的。不過這裡管理的是某個Scene的狀態。
swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)
- 這個方法將建立新的UIWindow,設定Root ViewController,並且使得這個window為keyWindow並展示。
swift
func sceneDidBecomeActive(_ scene: UIScene)
- 當Scene從一個inactive的狀態轉變為active的狀態時該方法被呼叫。
swift
func sceneWillEnterForeground(_ scene: UIScene)
- 當這個Scene從後臺轉移到前臺時,該方法被呼叫。使用該方法恢復一些在進入後臺時的改變。
swift
func sceneDidEnterBackground(_ scene: UIScene)
- 當這個Scene從前臺進入後臺時,該方法被呼叫。使用該方法儲存資料,釋放共享資源,以及儲存scene特有的狀態資訊等等
swift
func sceneDidDisconnect(_ scene: UIScene)
- 當該Scene被系統釋放時,此方法被呼叫。在進入後臺後不久,或者這個session被discarded之後,此方法被呼叫。釋放和該Scene相關聯的資源,在下次連線的時候,Scene將會被重建。
如何使用UIWindow實現小窗?
講了這麼一大堆廢話,其實主要是梳理在iOS 13.0之後,Apple對於App Delegate的職責分離,那麼接下來進入正題,如果我們要實現小窗,在這種職責分離的場景下,我們需要做什麼?
基於AppDelegate
什麼叫基於AppDelegate呢?就是說還是之前那套Window的概念,而不是新的Scene的概念,那麼這種情況下,我們就應該將SceneDelegate移除,如何移除呢?很簡單,分三步: 1. 刪除專案info.plist檔案中的Application Scene Manifest的配置資料。 2. 刪除AppDelegate中關於Scene的代理方法 3. 刪除SceneDelegate類
最後需要在AppDelegate中新增UIWindow
屬性,然後進行我們熟悉的UIWindow的初始化流程:
```swift class AppDelegate: UIResponder, UIApplicationDelegate { 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 = ViewController()
self.window!.rootViewController = rootVC
self.window!.makeKeyAndVisible()
return true
}
} ```
OK,這是AppDelegate我們熟悉的初始化,那麼如果需要新增小窗呢?很簡單,我們建立一個UIWindow即可,只需要設定isHidden
為false即可。
swift
func setupSmallWindow() -> UIWindow {
let smallWindow = UIWndow.init(frame: CGRect.init(x: UIScreen.main.bounds.width - 98 - 10, y: UIScreen.main.bounds.height - 176 - (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0) - 10, width: 98, height: 176))
smallWindow.rootViewController = UIViewController()
smallWindow.isHidden = false
return smallWindow
}
當然如果需要新增一些特性,比如拖動手勢,比如雙擊的互動等等,這個後續基於當前UIWindow進行封裝即可。同時要注意的是,在這種上下文中,UIWindow初始化時必須要設定rootViewController
屬性。
基於SceneDelegate
基於SceneDelegate就是說,又要想使用多視窗的特性,又想在某個Scene上提供小窗功能,這個其實就是Scene上關聯多個UIWindows的例項。這個怎麼做呢?它和之前初始化UIWindow不同了,現在初始化UIWindow是需要指定Scene的。
所以具體來說我們需要做兩步操作:
- 在SceneDelegate的啟動方法中建立承載UIWindow的Scene
- 建立小窗Window,一定要管理Scene
```swift func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } windowScene.title = "main"
window = UIWindow.init(windowScene: windowScene)
window?.rootViewController = ViewController.init()
window?.makeKeyAndVisible()
setupNewWindow()
}
// 建立新的小窗 func setupNewWindow() { let scenes = UIApplication.shared.connectedScenes for scene in scenes { if scene.title == "main" { newWindow = UIWindow.init(frame: CGRect.init(x: 0, y: 0, width: 100, height: 100)) newWindow?.backgroundColor = UIColor.systemBlue newWindow?.windowScene = (scene as? UIWindowScene) newWindow?.isHidden = false } } } ```
這裡有三個點需要注意:
- 通過
title
屬性來區分不同的scene - 建立UIWindow的時候,需要指定windowScene
- 一定要設定UIWindow的
isHidden
屬性,將其設定為false
在scene的場景下,如果不設定為false的話,那麼這個小窗是不會顯示的。也就是說初始化的UIWindow其實是預設隱藏的。