前言
之前做过一个直播间的小窗需求,在用户进入到其它页面的时分,仍然能够观看直播。而诸如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默许会有三个办法,别离如下:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
- 此办法用于整个运用的发动,以及初始化的设置。
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration
- 当一个新的Scene被创立时该办法被调用,在发动时并不会调用该办法。
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的状况。
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)
- 这个办法将创立新的UIWindow,设置Root ViewController,而且使得这个window为keyWindow并展示。
func sceneDidBecomeActive(_ scene: UIScene)
- 当Scene从一个inactive的状况转变为active的状况时该办法被调用。
func sceneWillEnterForeground(_ scene: UIScene)
- 当这个Scene从后台转移到前台时,该办法被调用。运用该办法恢复一些在进入后台时的改动。
func sceneDidEnterBackground(_ scene: UIScene)
- 当这个Scene从前台进入后台时,该办法被调用。运用该办法保存数据,开释共享资源,以及存储scene特有的状况信息等等
func sceneDidDisconnect(_ scene: UIScene)
- 当该Scene被体系开释时,此办法被调用。在进入后台后不久,或许这个session被discarded之后,此办法被调用。开释和该Scene相相关的资源,在下次衔接的时分,Scene将会被重建。
怎么运用UIWindow完成小窗?
讲了这么一大堆废话,其实首要是梳理在iOS 13.0之后,Apple关于App Delegate的责任别离,那么接下来进入正题,假如咱们要完成小窗,在这种责任别离的场景下,咱们需求做什么?
根据AppDelegate
什么叫根据AppDelegate呢?便是说仍是之前那套Window的概念,而不是新的Scene的概念,那么这种情况下,咱们就应该将SceneDelegate移除,怎么移除呢?很简单,分三步:
- 删去项目info.plist文件中的Application Scene Manifest的装备数据。
- 删去AppDelegate中关于Scene的代理办法
- 删去SceneDelegate类
最后需求在AppDelegate中增加UIWindow
属性,然后进行咱们熟悉的UIWindow的初始化流程:
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即可。
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
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其实是默许躲藏的。
参阅
1、Understanding Scene Delegate & App Delegate
2、iOS13 Scene Delegate详解