在iOS 13上围绕Scene Delegate启动应用程序的顺序


概述

UIWindowSceneDelegate是iOS13中新引入的,并且在创建新项目时将class SceneDelegate作为模板生成。本文介绍此SceneDelegate和应用程序的启动顺序。有些部分我还不了解,所以我写了问题的内容。

场景概述

我将解释场景。请参阅官方文档以了解确切的详细信息。

场景是用于显示应用程序UI的窗口。可以在iPad上同时拆分和显示多个应用程序,但是不能同时显示同一应用程序。在iOS 13中可以实现。此时,划分的屏幕之一对应于场景。多次显示同一个应用程序时,内部仅作为一个应用程序启动一个进程,但是会生成多个Scene对象,并且用户可以为每个屏幕进行独立的屏幕转换,这是一种可以使用的机制。

我写了

窗口,但这是一个描述性的概念,而不是UIKitUIWindow。表示场景的UIScene类具有UIWindowScene子类,而UIWindowScene通常在iOS上使用。 (我看不到任何其他子类,但我想知道它们有什么),对于UIWindowScene,可以将一个UIScreen和多个UIWindow绑定到该场景。

Info.plist场景清单

如果使用

Xcode11创建新项目或操作应用程序目标General > Deployment InfoRequires full screenSupports multiple windows复选框,则将在Info.plist中创建项目Application Scene Manifest。对于原始密钥,它是UIApplicationSceneManifest。 iOS13根据此条目是否存在或是否定义了稍后描述的UIApplicationDelegate方法来切换应用程序的操作模式是否支持场景。当然,在iOS12环境中,即使有该条目,它的行为也会像以前一样。

启动应用

以前,在应用程序启动时,填充UIApplicationDelegate并被赋予@UIApplicationMain的类型作为委托连接到UIApplication。此类模板生成的名称为AppDelegate。和

1
optional func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool

1
optional func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool

被调用。此部分在iOS13中相同。可以在此处完成应用程序流程级别的生命周期处理。

此外,如果将情节提要设置为General > Deployment Info > Main Interface,将对其进行读取并生成UIWindow,并且window.rootViewController设置将自动初始化。如果未设置情节提要,我可以自己生成并初始化UIWindow,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(_ application: UIApplication,
                     willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]?)
        -> Bool
    {
        let window = UIWindow(frame: UIScreen.main.bounds)
        self.window = window
        window.makeKeyAndVisible()

        let vc = ViewController()
        window.rootViewController = vc
        return true
    }
}

在上面的代码中,通过在AppDelegate.window中注册UIWindow来指定主窗口,并指定键窗口并通过window.makeKeyAndVisible()显示该窗口。

但是,从iOS13开始,启用场景支持时将忽略这些过程。指定为Main Interface的情节提要不会加载。同样,从代码中设置AppDelegate.window不会弹出窗口。窗口处理必须由Scene启动过程处理。

场景配置

启动

场景时,UIScene对象是在iOS端构建的,但是那时,将执行指定如何构建它的配置。可以通过Info.plist中的Application Scene Manifest > Scene Configuration(原始键:UISceneConfigurations)或UIApplicationDelegate中的

来指定。

1
optional func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration

指定。根据作为参数给出的UISceneSessionUIScene.ConnectionOptions完成设置,但是主键似乎是UISceneSessionrole: UISceneSession.Role。这是因为每个角色将Info.plist设置为Scene Configuration的子元素。但是,实际上实际上仅使用Application Session Role(原始密钥:UIWindowSceneSessionRoleApplication,代码:UISceneSession.Role.windowApplication)。

配置的类型为UISceneConfiguration,并在初始化程序中接受name: String?sessionRole: UISceneSession.Role。似乎该角色应该转移作为参数接收的UISceneSession的值。可以设置的其他属性是sceneClass: AnyClass?delegateClass: AnyClass?storyboard: UIStoryboard。在Info.plist中,可以分别将其设置为Configuration Name(UISceneConfigurationName)Class Name(UISceneClassName)Delegate Class Name(UISceneDelegateClassName)Storyboard Name(UISceneStoryboardFile)

使用

Xcode11创建新项目时,默认情况下将Configuration Name = Default ConfigurationDelegate Class Name = $(PRODUCT_MODULE_NAME).SceneDelegateStoryboard Name = Main作为模板设置为Info.plist

可以同时设置

委托方法和Info.plist,在这种情况下,两者的内容将被合并。根据委托方法返回的UISceneConfigurationnamesessionRole搜索Info.plist条目,如果sceneClassdelegateClassstoryboardnil,则似乎使用了条目的值。如果没有委托方法,则Info.plist条目将采用每个角色数组的[0]元素。在新创建的项目中,也实现了该委托方法,并且UISceneConfiguration中的name设置为Default Configuration

在处理多个配置时,最好在Info.plist端静态定义条目,并使用name指定在委托方法中选择哪个条目。

配置重载

使用

iOS13.0的iPad模拟器进行验证时,启动应用程序时可能不会调用设置Configuration的委托方法。可能有一些缓存功能。

如果没有

Info.plist配置条目,则在重新启动进程时将调用委托方法。但是,如果Info.plist中有一个条目,则即使重新启动该进程也不会调用该条目。每当我对Info.plist进行更改时,无论进程是否重新启动都将调用它。由于AppDelegate的启动方法是在每次执行调试时都被调用的,因此我认为该过程无论如何都将重新启动,因此我不确定,但是通过在iPad端显式终止应用程序,结果似乎有所变化。

我认为机会不多,但是在使用"场景配置"时可能需要小心。

发射场景

决定

配置后,iOS将基于该场景生成一个场景。当场景开始时,UISceneDelegate

1
optional func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)

被调用。此处连接的委托对象是在配置中指定的类型delegateClass的对象。在模板中为Scene Delegate。因为此委托方法用于UISceneDelegate,所以scene的类型为UIScene,但是实际上要出现的是UIWindowScene的对象,委托类型应该为UIWindowSceneDelegate。如果在"配置"中指定了Storyboard,则会自动生成UIWindow并在此处设置window.rootViewController。如果您未设置Storyboard,则可以在此处的代码中对其进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene,
               willConnectTo session: UISceneSession,
               options connectionOptions: UIScene.ConnectionOptions)
    {
        guard let scene = (scene as? UIWindowScene) else {
            return
        }
        let window = UIWindow(windowScene: scene)
        self.window = window
        window.makeKeyAndVisible()

        let vc = ViewController()
        window.rootViewController = vc
    }
}

首先,在开头检查scene的类型。生成UIWindow使用一种称为init(windowScene:)的新方法,而不是init(frame:)。除了创建和初始化窗口外,还可以通过在UIWindowSceneDelegateoptional var window: UIWindow? { get set }属性中设置生成的窗口来将其注册为主窗口。它与UIApplicationDelegate中的传统代码初始化方法非常相似,我认为您可以捕获Scene的概念。

场景重用?

根据

模板代码注释中的// This delegate does not imply the connecting scene or session are new (see application:configurationForConnectingSceneSession instead).UISceneUISceneSession对象可能会被重用。生命周期文档的图中还有一个scene的重用图。也许,除了第一个Unattached → Foreground Inactive之外,Background → Unattached --(ここ)-> Foreground InactiveBackground → Unattached --(ここ)-> Background之间的第二个和后续转换将为同一UIScene对象调用willConnectTo。但是,在我尝试了一段时间的范围内,我找不到实际生成它的过程。

如果UIScene被重用,则关联的UISceneDelegate也应被重用,因此似乎保留了先前设置的window属性。在那种情况下,我想知道是否应该检查并重新使用窗口或视图树,或者是否可以重新生成整个窗口。

先前的iOS支持

如果使用从

模板新创建的项目按原样将Deployment Target降低到iOS 12,则会经常发生编译错误。作为对此的简单响应,似乎可以做以下两点。

@available添加到SceneDelegate

1
2
3
4
@available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    // 略
}

@available添加到AppDelegateapplication(:configurationForConnecting:options:)中。

1
2
3
4
5
6
7
8
9
10
class AppDelegate: UIResponder, UIApplicationDelegate {
    // 略
    @available(iOS 13.0, *)
    func application(_ application: UIApplication,
                     configurationForConnecting connectingSceneSession: UISceneSession,
                     options: UIScene.ConnectionOptions) -> UISceneConfiguration
    {
        // 略
    }
}

并且在使用Storyboard时,对于iOS12或更早版本,设置Main Interface,对于iOS13或更高版本设置Application Scene Manifest

使用

代码进行构建时,对于iOS12或更早版本,必须在application(:willFinishLaunchingWithOptions)中执行启动过程;对于iOS13或更高版本,必须在scene(:willConnectTo:options)中执行启动过程,但是即使是iOS13,也将调用application(:willFinishLaunchingWithOptions),因此在iOS 13的情况下,有必要防止重复的启动过程。总而言之,我认为将是如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(_ application: UIApplication,
                     willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]?)
        -> Bool
    {
        if #available(iOS 13, *) {
        } else {
            let window = UIWindow(frame: UIScreen.main.bounds)
            self.window = window
            window.makeKeyAndVisible()

            let vc = ViewController()
            window.rootViewController = vc
        }
        return true
    }
}

@available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene,
               willConnectTo session: UISceneSession,
               options connectionOptions: UIScene.ConnectionOptions)
    {
        guard let scene = (scene as? UIWindowScene) else {
            return
        }
        let window = UIWindow(windowScene: scene)
        self.window = window
        window.makeKeyAndVisible()

        let vc = ViewController()
        window.rootViewController = vc
    }
}

适当总结常用部分是一个好主意。如上所述,即使在iOS 13中也可能无法启用Scene,因此仅靠版本判断是不够的,但是它会很复杂,并且您不知道Scene有效性判断的确切部分,因此您不必这样做。此外,如果您不介意浪费大量产生UIWindow和视图控制器的成本,则可以两次生成它而无需分支。