iOS13のSceneDelegate周りのアプリの起動シーケンス

# 概要

iOS13から新たに`UIWindowSceneDelegate`が導入され、プロジェクトを新規作成すると`class SceneDelegate`がテンプレートとして生成されるようになりました。この記事では、この`SceneDelegate`とアプリの起動シーケンスについて説明します。まだわかっていない部分もあるので、そこは疑問の内容を書きました。

# Sceneの概要

Sceneについてザックリと説明します。正確なところは[公式の文書](https://developer.apple.com/documentation/uikit/app_and_environment/scenes)を参照してください。

SceneとはアプリのUIを表示するための窓の事です。iPadでは以前から複数のアプリを同時に分割表示する事ができましたが、同一のアプリを同時に複数表示する事はできませんでした。iOS13からはこれが可能になります。この時、分割されている画面1つがSceneに対応します。同一のアプリが複数表示されているときは、内部的にアプリケーションとしては1つのプロセスだけが起動していますが、Sceneオブジェクトは複数生成されており、ユーザはそれぞれの画面ごとに独立した画面遷移を利用できるというしくみです。

窓と書きましたが、これは説明上の概念であって、`UIKit`の`UIWindow`のことではありません。Sceneを表す`UIScene`クラスには`UIWindowScene`サブクラスがあり、iOSでは`UIWindowScene`が普段使われています。(他のサブクラスが見当たらないのですが、どんな物があるのか気になります)そして、`UIWindowScene`の場合は、その1つのSceneに対して1つの`UIScreen`と複数の`UIWindow`が紐付けられます。

# Info.plistのシーンマニフェスト

Xcode11でプロジェクトを新規作成したり、アプリターゲットの`General > Deployment Info`の`Requires full screen`や`Supports multiple windows`のチェックボックスを操作すると、`Info.plist`の中に`Application Scene Manifest`という項目が作られます。Raw Keyでは`UIApplicationSceneManifest`です。iOS13は、このエントリがあるかどうか、もしくは後述する`UIApplicationDelegate`のメソッドが定義されているかどうかによって、アプリの動作モードのScene対応の有無を切り替えます。もちろん、iOS12環境ではこのエントリがあったとしても、従来と変わらない動作をします。

# アプリの起動

従来はアプリが起動すると`UIApplicationDelegate`を満たし、`@UIApplicationMain`が付与されている型が、`UIApplication`にデリゲートとして接続されました。この型のテンプレートが生成する名前は`AppDelegate`です。そして、

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

```swift
optional func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool
```
が呼ばれます。iOS13でもこの部分は同一です。アプリケーションプロセスのレベルでのライフサイクル処理はここで行う事ができます。

更に、`General > Deployment Info > Main Interface`としてStoryboardが設定されている場合はこれが読み込まれて、`UIWindow`の生成と、`window.rootViewController`の設定などの初期化が自動で行われていました。Storyboardが設定されていない場合は、下記のようにして自分で`UIWindow`の生成と初期化を行う事ができました。

```swift
@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からは、Scene対応が有効になっている場合にはこれらの処理が無視されるようになりました。`Main Interface`として指定されているStoryboardは読み込まれません。また、コードから`AppDelegate.window`を設定してもウィンドウは表示されません。ウィンドウの取り扱いはSceneの起動処理の方で対応する必要があります。

# Sceneのconfiguration

Sceneの起動において、`UIScene`オブジェクトの構築はiOS側が行いますが、その際にどのように構築するかを指定するconfigurationが行われます。これは、`Info.plist`の`Application Scene Manifest > Scene Configuration`(Raw Key: `UISceneConfigurations`)で指定するか、`UIApplicationDelegate`の

```swift
optional func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration
```
で指定します。設定は引数として与えられる`UISceneSession`や`UIScene.ConnectionOptions`に応じて行いますが、主要なキーとなるのは`UISceneSession`の`role: UISceneSession.Role`のようです。というのも、`Info.plist`では`Scene Configuration`の子要素として、このロールごとに設定を行うようになっているからです。ただ、実質的に普段使用するのは`Application Session Role`(Raw Key: `UIWindowSceneSessionRoleApplication`, コード: `UISceneSession.Role.windowApplication`)だけのようです。

Configurationは型としては`UISceneConfiguration`であり、initializerで`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でプロジェクトを新規作成すると、テンプレートとしてデフォルトで`Info.plist`に`Configuration Name = Default Configuration`, `Delegate Class Name = $(PRODUCT_MODULE_NAME).SceneDelegate`, `Storyboard Name = Main`が設定されています。

デリゲートメソッドと`Info.plist`の両方を設定する事ができて、その場合は両方の内容が合成されます。デリゲートメソッドで返した`UISceneConfiguration`の`name`と`sessionRole`に基づいて`Info.plist`のエントリが検索され、`sceneClass`, `delegateClass`, `storyboard`が`nil`の場合は`Info.plist`のエントリの値が使われるようです。デリゲートメソッドがない場合は、`Info.plist`のエントリはロールごとの配列の`[0]`要素が採用されます。新規作成したプロジェクトでは、このデリゲートメソッドも実装されていて、`UISceneConfiguration`の`name`を`Default Configuration`に設定するようになっています。

もし複数のConfigurationを取り扱う場合は、`Info.plist`側にエントリを静的に定義しておいて、デリゲートメソッドではそのどれを選択するかを`name`で指定するだけにする構成が良さそうな気がします。

## Configurationのリロード

`iOS13.0`のiPadシミュレータで検証したところ、アプリケーション起動時に`Configuration`を設定するデリゲートメソッドが呼び出されない場合がありました。なんらかのキャッシュ機能があるかもしれません。

`Info.plist`のConfigurationエントリが無い場合は、プロセスを再起動すると必ずデリゲートメソッドが呼ばれました。しかし、`Info.plist`にエントリがある場合は、プロセスを再起動しても呼ばれない事がありました。`Info.plist`に変更を与えた場合は、プロセスの再起動に関わらず必ず呼ばれました。デバッグ実行のたびに`AppDelegate`のlaunch系のデリゲートメソッドは呼ばれていたのでプロセスはいずれにせよ再起動していると思うのでよくわからないですが、iPad側でアプリを明示的に終了する事で結果が変わるようでした。

あまり機会は多くないと思いますが、SceneのConfigurationをいじくるときは注意が必要かもしれません。

# Sceneの起動

Configurationが決まったらそれに基づいてiOSがSceneを生成します。そして、Sceneが起動する際には、`UISceneDelegate`の

```swift
optional func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)
```
が呼ばれます。ここで接続されているデリゲートオブジェクトは、Configurationで指定した`delegateClass`の型のオブジェクトです。テンプレートでは`Scene Delegate`になっています。このデリゲートメソッドは`UISceneDelegate`のものなので、`scene`の型が`UIScene`になっていますが、実際に来ているのは`UIWindowScene`のオブジェクトで、デリゲートの型は`UIWindowSceneDelegate`にしておきます。もし、ConfigurationでStoryboardを指定していた場合は、ここで自動的に`UIWindow`の生成や`window.rootViewController`の設定などが行われます。Storyboardを設定しなかった場合は、ここでコードで初期化する事ができます。

```swift
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(frame:)`ではなく`init(windowScene:)`という新しいメソッドを使用します。ウィンドウの生成と初期化の他、`UIWindowSceneDelegate`の`optional var window: UIWindow? { get set }`プロパティに生成したウィンドウを設定する事でメインウィンドウとして登録しています。従来の`UIApplicationDelegate`でのコードによる初期化の方法とよく似ており、Sceneの概念が捉えられると思います。

## Sceneの再利用？

テンプレートコードのコメントの`// This delegate does not imply the connecting scene or session are new (see application:configurationForConnectingSceneSession instead).`から、`UIScene`と`UISceneSession`のオブジェクトは再利用される事がありそうです。また、[ライフサイクルの文書](https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle)の図でも`Scene`の再利用の図があります。おそらく、初回の`Unattached → Foreground Inactive`に加えて、2回目以降では`Background → Unattached --(ここ)-> Foreground Inactive`と`Background → Unattached --(ここ)-> Background`の遷移で、`willConnectTo`が同じ`UIScene`オブジェクトに対して呼ばれるのだと思います。しかし、しばらく試した範囲では実際に発生させる手順がわかりませんでした。

もし`UIScene`が再利用される場合、紐付いている`UISceneDelegate`も再利用されるはずですから、`window`プロパティに以前設定したものが残っていそうです。その場合これをチェックしてウィンドウやビューツリーを再利用すべきなのか、それとも、新しくウィンドウごと再生成してしまって問題ないのかどうか、よくわからないので気になっています。

# 以前のiOSのサポート

テンプレートから新規作成されたプロジェクトそのままでDeployment TargetをiOS12に下げるとコンパイルエラーが多発します。これの簡単な対応としては下記の2点を行うのが良さそうです。

### `SceneDelegate`に`@available`をつける。

```swift
@available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
	// 略
}
```

### `AppDelegate`の`application(:configurationForConnecting:options:)`に`@available`をつける。

```swift
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)`は呼ばれてしまうので、iOS13の場合に重複して起動処理が行われないようにする必要があります。まとめると、下記のようになるかと思います。

```swift
@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
    }
}
```

共通部分は適宜まとめると良いでしょう。前述したとおりiOS13でもSceneが有効にならない場合があるので本当はバージョン判定だけでは不足がありますが、煩雑になるしSceneの有効判定の正確なところはわからないのでやらなくて良いでしょう。また、`UIWindow`やViewControllerの無駄な生成のコストを気にしないのであれば、分岐しないで2回生成してしまうというのもありかもしれません。