前言

之前做过一个直播间的小窗需求,在用户进入到其它页面的时分,仍然能够观看直播。而诸如bilibili的视频,微信的视频号,网易云音乐的广场等手机端视频小窗,在iOS 14发布之后,都运用了Apple官方的画中画功用来完成小窗播放。

如何在使用SceneDelegate的项目中实现小窗需求?

具体表现如上,能够具有许多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移除,怎么移除呢?很简单,分三步:

  1. 删去项目info.plist文件中的Application Scene Manifest的装备数据。
  2. 删去AppDelegate中关于Scene的代理办法
  3. 删去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的。

所以具体来说咱们需求做两步操作:

  1. SceneDelegate的发动办法中创立承载UIWindow的Scene
  2. 创立小窗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
        }
   }
}

这儿有三个点需求留意:

  1. 经过title 属性来区分不同的scene
  2. 创立UIWindow的时分,需求指定windowScene
  3. 一定要设置UIWindow的isHidden 属性,将其设置为false

在scene的场景下,假如不设置为false的话,那么这个小窗是不会显示的。也便是说初始化的UIWindow其实是默许躲藏的。

参阅

1、Understanding Scene Delegate & App Delegate

2、iOS13 Scene Delegate详解