[iOS13] UIScene APIを使用する [Xcode11]

Xcode11では、この`UIScene API`のライフサイクルを使用したものが、デフォルトテンプレートに採用されています。現時点では、UIApplicationベースでも問題ないものの、UIApplicationのUIに関するメソッドが非推奨となっていることから、今後このシーンの使用が一般的になると思われます。現時点でのメモを共有したいと思います。

## UISceneとは
iOS12 でのアプリケーションのプロセスは１つで、それに対するUIインスタンス（ウインドウ）も１つでした[^1]。iOS13（UIScene）以後のアプリケーションのプロセスは１つで、それに対するUIインスタンス（ウインドウ）は複数です。**マルチウインドウ**と言います。開発者視点では、そのウインドウを**シーン**と呼びます。UIScene API とは、ひとまずアプリをマルチウインドウ（マルチシーン）に対応するAPIと言えそうです。

## ユーザーによるマルチウインドウ（マルチシーン）の起動方法
iOS13で**マルチウインドウ対応をしたアプリは、iPadで単独のアプリから複数のウインドウを起動し表示することが可能**になります。ウインドウを追加起動する操作方法を紹介します。

####システムにより提供された方法
iOS9から導入されているiPadのマルチタスク機能「Split View」「Slide Over」と、特定のアプリのウインドウ一覧を表示する「App Expose」です。

|Split View|Slide Over|App Expose|
|---|---|---|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/8636f93f-8c40-1d78-707a-f922d6d201c2.gif">|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/af8b4bef-1c18-8228-6166-87dfdb5e94a9.gif">|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/7a5d8970-bbd6-4296-75ec-5d93b2ddaea9.png">|

- [iPad でマルチタスク機能を使う](https://support.apple.com/ja-jp/HT207582)
- [Slide Over and Split View Quick Start](https://developer.apple.com/library/archive/documentation/WindowsViews/Conceptual/AdoptingMultitaskingOniPad/QuickStartForSlideOverAndSplitView.html)
- App Expose

####アプリにより提供する方法
アイコンのドラッグ以外のカスタムなウインドウ起動を、アプリで実装することも出来たりします。標準アプリでの例を紹介します。

- Safari内の【タブ】や、メールappで一覧の【セル】を、画面の端にDrag＆DropしSplit Viewを起動する。
- Safariで URLリンク をロングタップすると出る「新しいウインドウで開く」のポップアップを選択し起動する。

## APPスイッチャーの変化

iPadのAPPスイッチャーでは、切り替えられるUIの単位が変わっています。**iOS12以前は「起動中のアプリ」**を指していましたが、**iOS13以降では「アプリのシーン」**を切り替えることが出来るようになりました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/244bf6fd-33f1-e0ea-d693-991d2736460d.png" width="1190">

標準アプリでは、Safari、メッセージ、メール、カレンダー、マップなどがマルチウインドウに対応していることを確認しました。（すべてではなく未対応のものもありました。）
WWDC2019にて発表された、MacOS Catalinaでの [Mac Catalyst](https://developer.apple.com/jp/mac-catalyst/) によりiPadアプリをMacで実行可能になり「マルチウインドウ化にはこのAPIが必須だった」とのことです。

## マルチウインドウに対応させる意義

- ドキュメント系のアプリを並べて表示させられるようになる。
- 一つ前の状態を消さずに、別の処理を行える。（例えば、地図アプリで別のルートや場所を同時に表示させたい時。）
- Slide Over上にウインドウを並べてTo Do代わりに使用する。（例えば、メールappなどで、書きかけのメールなどを並べます。）
- 他方を参照しなばら、もう一方を利用したい時。（例えばメッセージappなどで、他のスレッドを参照しながら会話を行うなど。）
- アプリ内でのデータ移動に利用する。（例えば、カレンダーappでマルチウインドウ上で別の月に予定をDrag&Dropする）

## シーンの採用によるView Hierarchyの変化

<img width="1190" alt="UIScene API.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/20a06167-3ea8-b7d8-8e13-fe298cf40b62.png">
`UIScene API`を採用すると、ビュー階層が変わり`UIScreen`と`UIWindow`の間に[`UIWindowScene`](https://developer.apple.com/documentation/uikit/uiwindowscene)が入ります。

iOSアプリのビュー階層を示したものかと思われます。
一番下のは端末の画面オブジェクト [`UIScreen.main`](https://developer.apple.com/documentation/uikit/uiscreen/1617815-main)です。その次の、新たに加わった`UIWindowScene`は、View階層のトップレベルオブジェクトとなりました。アプリのひとつのUIインスタンスを管理します。`windows: [UIWindow]`プロパティを持ち、関連付けられている`UIWindow`の参照を取得できます。この中のひとつには`SceneDelegate`の`var window: UIWindow?`プロパティで保持するインスタンスが含まれるはずです。
複数の`UIView`は`UIViewController`上のインスタンスです。`UIViewController`のルートオブジェクトは`window.rootViewController`で保持されます。

####Xcodeのビューデバッガの比較

|（iOS12）ビューデバッガ|表示画面|
|---|---|
|<img width="1437" alt="スクリーンショット 2019-10-08 14.48.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/0d317728-74ee-0af4-1f1c-51a055b27dbf.png">|![Simulator Screen Shot - iPad Pro (11-inch) - 2019-10-08 at 14.42.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/9d88a287-9de5-11cf-d897-8a2259dfe7ef.png)|

|（iOS13)ビューデバッガ|表示画面|
|---|---|
|<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/5212a9fc-715e-8e02-65c4-b908e2cc27a9.png">|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/89e5931b-15c8-8986-06fb-4ca4fe3136f0.png" width="600">|

iOS13（UIScene採用したもの）のキャプチャで`UIWindowScene`が追加されていることがわかります。また、あえてSplit View表示にして確認したのですが、この時**片側の画面（スペース）が個別のシーン**(`UIWindowScene`)となります。冒頭のAPPスイッチャー上のイメージ図では「スナップショットが１つのシーン」と簡略化して説明されているように思うのですが、「Split Viewのウインドウは、片側の画面ずつ２つのシーン」と捉えられそうです。

## シーンとセッション
[`UIWindowScene`](https://developer.apple.com/documentation/uikit/uiwindowscene)は、1つまたは複数のUIWindowを含む、アプリのUIの1つのインスタンスの管理、ライフサイクルを管理します。シーンがバックグラウンドに移行しインタラクティブでなくなると、システム側で自動的に破棄が行われます。

[`UISceneSession`](https://developer.apple.com/documentation/uikit/uiscenesession)は、ユーザーが新規シーンの追加を行うと、セッションが作成され、シーンを追跡します。セッションは、[一意な識別子](https://developer.apple.com/documentation/uikit/uiscenesession/3197956-persistentidentifier)と[シーンコンフィグレーション](https://developer.apple.com/documentation/uikit/uiscenesession/3197955-configuration)を含みます。ユーザー操作の状態[UserActivity](https://developer.apple.com/documentation/uikit/uiscenesession/3197959-staterestorationactivity)を持っています。ユーザーがアプリスイッチャーでシーンを閉じることに応じて破棄されます。システムによりシーンが開放されてもセッションは残っていて、状態復元に使用されます。

## ライフサイクルの変化

iOS12 でのアプリケーションのプロセスは１つで、それに対するUIインスタンスも１つでした。「プロセスのライフサイクル（起動や終了）」「UIの状態のライフサイクル」すべてをシステムは`AppDelegate`に通知していました。
実際のアプリの`AppDelegate`では、ワンタイムの非UIの処理（データベースへの接続やデータ構造の初期化など）を行ったあと、UIのセットアップの処理を行う、全てが行われていました。

iOS13（UIScene）以後のアプリケーションのプロセスは１つで、それに対するUIインスタンスは複数です。プロセスは複数のシーンセッションに共有されます。
それに伴い`AppDelegate`の責任は変わります。「プロセスのライフサイクル（起動や終了）」のみになり、新しい`SceneDelegate`が「UIの状態のライフサイクル」の責任を担います。（そして`AppDelegate`には新しく「シーンの作成・破棄」の責任が加わります。）
UIのセットアップや、不要になったUIの取り外しの処理は`SceneDelegate`で行うようにします。

|iOS12 / iOS13 (UIScene)|
|---|
|<img width="1000" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/543a0b39-62ed-5fd7-1f1a-d3bac207ae69.png">|
|<img width="1000" alt="76bdb54d-905a-bc9b-15ba-bc08fac93efc.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/268a0994-c321-d3dc-550b-ffc9f41e65c0.png">|

iOS13での、シーンライフサイクルを採用すると、「UIの状態のライフサイクル」に関する`AppDelegate`のデリゲートメソッドは呼びだしはされません。バックグラウンドやフォアグラウンドといったUIの状態は`SceneDelegate`へと通知されるようになります。移行するメソッドの内容はたいてい1対1となるためシンプルです。これらのデリゲートメソッドは移行が必要です。

|iOS12 → iOS13 (UIScene)|
|---|
|<img width="1000" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/8cad2d7e-df61-b517-8f53-658fbb14e2e0.png">|

iOS13で、マルチウインドウを採用しても、iOS12以前のサポートは可能です。単に両方のメソッドを保持して、実行時にUIKitが適切な方を呼び出します。
（iOS12以下にターゲットを変更、コンパイラのエラーFixサジェストが参考になります。）

## UIScene API の採用手順
[Supporting Multiple Windows on iPad](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/supporting_multiple_windows_on_ipad)のサンプルコードを参考に、具体的な手順をみていきます。

- `App TARGETS の General` > `[Development Info]の[Supports multiple windows]`チェックボックスを**有効**にする
- チェックボックスを有効にすると、XcodeによりInfo.plistファイルに`UIApplicationSceneManifest`キーが追加される。

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/157fba0b-8f98-7984-c24b-8e4920f0325b.png">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/6c4b4b68-5402-3979-108e-014bc3ff5c5a.png">

このキーが追加されると、`UIScene API`を用いた新しいライフサイクルメソッドが呼ばれるようになります。

- [Enable Multiple Windows](UIApplicationSupportsMultipleScenes) 項目を`NO`にすると「UISceneのライフサイクルは使用するが、マルチウインドウ化はしない」となります。先ほどONにした`Supports multiple windows`のチェックが**OFF**になります。
- その他の項目については [Specifying the Scenes Your App Supports](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/specifying_the_scenes_your_app_supports)を参照

シーンの構成([UISceneConfiguration](https://developer.apple.com/documentation/uikit/uisceneconfiguration))は、次の２つの方法でシステムに提供します。

(A) Info.plistにシーン構成を定義して提供。
(B) UIApplicationのデリゲートメソッドを実装してシーン構成を提供。

<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/e6344437-3e5f-a38d-8565-e1c5a6709447.png">

#### (A) Info.plistにシーン構成を定義
```XML:Info.plist
<key>UIApplicationSceneManifest</key>
	<dict>
		<key>UIApplicationSupportsMultipleScenes</key>
		<true/>
		<key>UISceneConfigurations</key>
		<dict>
			<key>UIWindowSceneSessionRoleApplication</key>
			<array>
				<dict>
					<key>UISceneConfigurationName</key>
					<string>Default Configuration</string>
					<key>UISceneDelegateClassName</key>
					<string>$(PRODUCT_NAME).SceneDelegate</string>
					<key>UISceneStoryboardFile</key>
					<string>Main</string>
				</dict>
			</array>
		</dict>
	</dict>
```
#### (B) UIApplicationのデリゲートメソッドを実装してシーン構成を提供。
動的に行う必要がある場合は [application(_:configurationForConnecting:options:)](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/3197905-application) で UISceneConfiguration オブジェクトを返します。(A)のplist相当のコンフィグレーションは以下のようになります。
引数は`UIViewController`等ではなく`UIStoryboard`となっているところが、個人的には注目ポイントだと思いました。（画面表示に必要な`UIWindow`と`UIViewController`の両方をインスタンス化する仕組みを利用し、同時に提供できるようにしていると推察します。）

```Swift:コードでのUISceneConfiguration作成
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        let configuration = UISceneConfiguration(name: "Default Configuration", sessionRole: .windowApplication)
        configuration.delegateClass = SceneDelegate.self
        configuration.storyboard = UIStoryboard(name: "Main", bundle: .main)
    }
```

#### SceneDelegate.swiftファイルの作成

Info.plist内でデリゲート先クラスとして指定した`SceneDelegate.swift`ファイルを以下のように作成します。

```Swift:UIの状態が復元される最小限のSceneDelegate
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {   
    // (1) windowインスタンスはシーンごとに保持する
    var window: UIWindow? 
    // (2) シーン切断時に呼ばれます。保存するシーンuserActivityを返します
    func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
        return scene.userActivity
    }
    // (3) シーン接続時に呼ばれます。保存されたuserActivityを元にUIを復元します
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // 得られるuserActivityから具体的なUI復元（画面遷移など）を書く
        if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
            if !configure(window: window, with: userActivity) {
                print("Failed to restore DetailViewController from \(userActivity)")
            }
        }
        // アクティビティが無い場合は、何もする必要なし。コンフィグレーションで指定したStoryboardの初期VCが起動します。
    }
}
```

```Swift:PhotoDetailViewController
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // (4)
        view.window?.windowScene?.userActivity = photo?.openDetailUserActivity
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // (4)
        view.window?.windowScene?.userActivity = nil
    }
```

(1) UIWindow?プロパティを宣言。iOS12までは通常、AppDelegateで保持していたところ、windowインスタンスはシーンごとにSceneDelegate で保持します。指定した`Main.storyboard`のイニシャルVC呼び出し時に自動的に代入されます。

(2) アプリがバックグラウンドに入り非アクティブになった時に呼ばれます。UI状態の復元が必要なシーンは、このメソッドで`scene.userActivity`を返します。ここで渡したuserActivityはシステムにより永続化され、セッションが消されるまでUIKitにより維持されます。状態復元が不要なsceneでは`nil`を返せばよいと思われます。
シーンの切断状態が続くとメモリ開放のためシステムによりシーンは破棄され、セッションだけが残ります。ユーザーがAPPスイッチャに見えているのはシーンの**スナップショット**なので、このメソッドで保存してない場合、そのシーンを復元するためのUserActivityが`scene(willConnectTo:session:options)`で得られなくなります。

(3) シーン接続時に呼ばれます。シーンがセッションと接続する時、 [UIScene.ConnectionOptions](https://developer.apple.com/documentation/uikit/uiscene/connectionoptions) と`UISceneSession`の参照が得られます。いずれかのuserActivityからUI復元処理を行います。他のシステムからのUserActivityの受け渡しがない、または、アプリで（前回）保存したUserActivityがない場合は、どちらもnilになります。

Appleのサンプルコードでは、特別にマルチウインドウに関係のないものもシーンベースに移行しているようで、いくつか見たところでは以下のような実装が定番となっています。

- `connectionOptions.userActivities.first`から取得できるかチェックして、次に`session.stateRestorationActivity`を取得するのがお決まりのようです。

私が調査したところによると、たいてい、同じユーザーアクティビティが入るのではと思います。詳しくは ー＞ [【iOS13】scene(_:willConnectTo:options:)のオプションとセッションの NSUserActivity の違いについて](https://qiita.com/snoozelag/items/1cdea93ef1f184336ca6)

- `configure(window:with:)`は自前のメソッドです。遷移の有無を返す戻り値のBoolは、利用しないパターンも見受けられましたので、@discardableResultを追記しても良さそうです。

```Swift:configure(window\:with\:)のサンプル
   @discardableResult
   private func configure(window: UIWindow?, with activity: NSUserActivity) -> Bool {
        if let detailViewController =  UIStoryboard(name: "Main", bundle: .main).instantiateViewController(withIdentifier: "DetailViewController") as? DetailViewController {
            if let navigationController = window?.rootViewController as? UINavigationController {
                navigationController.pushViewController(detailViewController, animated: false)
                detailViewController.restoreUserActivityState(activity)
                return true
            }
        }
        return false
    }
```


(4) UIの状態を復元したいポイントで`windowScene?.userActivity`に状態復元に必要な情報を作成したNSUserActivityを入れます。システム側に(２)のメソッドで提供するアクティビティオブジェクトになります。
`UIScene API`では`NSUserActivity`を利用し状態の保存と復元が行う手法を採用しました。[Handoff](https://developer.apple.com/handoff/) APIのクラスを借りてきたとのことです。
`view.window?.windowScene?.userActivity = photo?.openDetailUserActivity`で保持する内容は以下のようなものです。画面のパス、記事ID、URLといった情報を持たせると良さそうです。シーンの再接続時の状態復元に必要な情報をカプセル化します。

```Swift:NSUserActivityのサンプル
        let GalleryOpenDetailActivityType = "com.example.gallery.openDetail"
        let GalleryOpenDetailPath = "openDetail"
        let GalleryOpenDetailPhotoIdKey = "photoId"

        let userActivity = NSUserActivity(activityType: GalleryOpenDetailActivityType)
        userActivity.title = GalleryOpenDetailPath
        userActivity.userInfo = [GalleryOpenDetailPhotoIdKey: name]
```

以上が、`UIScene`の採用手順です。

## シーンベースでのコールスタック

イベントの概要や行うとよい処理を併記しています。

◾️ユーザーがアイコンをタップし、アプリの初回起動が行われました。デリゲートメソッドが次の順序で呼ばれます。

(1) [UIApplicationDelegate#application(_:didFinishLaunchingWithOptions:)](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622921-application)
・ワンタイムの非UIなセットアップ処理（データベース接続やデータ構造の初期化）を行う。

(2) [UIApplicationDelegate#application(_:configurationForConnecting:options:)](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/3197905-application)
・コンフィグレーションを指定し、シーンセッションの作成を行う。
・コンフィグレーションはコードで動的に設定をするか、Info.plistで静的に行う。
・Info.plistで静的に行った場合には、`name`引数で参照し、`connectingSceneSession.role`を渡す。（コード参照）
・コンフィグレーションには、メインシーン、アクセサリといった種類があり、アプリに併せて正しいコンテキスト選択に使用できる。
・SceneDelegateクラスの指定、初期Storyboardの指定、作成したいシーン（のサブクラス）の指定を行う。

```Swift:シーンのコンフィグレーション実装例
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions) -> UISceneConfiguration {
    return UISceneConfiguration(name: "Default", sessionRole: connectingSceneSession.role)
}
```
＜注意＞**シーンの新規作成**直前にしか呼ばれません。Xcodeで実行した際に毎回呼ばれるだろうと思っていたのですが、最初のシーン新規作成時だけ呼ばれて、次回以降そのセッションが生きている間、シーンがセッションへ再接続時する時はコールされないようです（多分、セッションに既にコンフィグレーションを持っているため）。
`UIApplication.shared.requestSceneSessionDestruction(session, options: nil, errorHandler: nil)`を実行して、意図的にセッション破棄を実行すると、**他に起動していないシーンがなければ**次回起動時にシーンが新規作成されコールされました（他のシーンがあればそちらが起動する模様）。また、マルチタスク機能でのシーン追加操作を行うことでも呼ばれました。

(3) [UISceneDelegate#scene(_:willConnectTo:options:)](https://developer.apple.com/documentation/uikit/uiscenedelegate/3197914-scene)

・この時点では、UIは作成されていないが、シーンセッションは作成され`SceneDelegate`と接続されている。

```Swift:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options: .ConnectionOptions) {
        window = UIWindow(windowScene: scene as! UIWindowScene)
        if let activity = options.userActivities.first ?? session.stateRestorationActivity {
            configure(window: window, with: activity)
        }
    }
}
```

◾️ここでユーザーがホームバーをスワイプして、ホームに戻ったとします。

(4) [UISceneDelegate#sceneWillResignActive(_:)](https://developer.apple.com/documentation/uikit/uiscenedelegate/3197919-scenewillresignactive)
(5) [UISceneDelegate#sceneDidEnterBackground(_:)](https://developer.apple.com/documentation/uikit/uiscenedelegate/3197917-scenedidenterbackground)

・使用途中のテキストの下書きのようなユーザーデータはシーンの再接続時のために削除せずに保存、または保存したままにしておきます。

この後、ある時点でシステムの判断によりシーン切断が起こりえます。メモリにはシーンに関連する多量のリソースが保持されており、割り当てを解除してリソースを回収するためです。

(5.5?) [UISceneDelegate#sceneDidDisconnect(_:)](https://developer.apple.com/documentation/uikit/uiscenedelegate/3197916-scenediddisconnect)

◾️ユーザーがAPPスイッチャーから、シーンを上にスワイプしキルしました。

(6) [UIApplicationDelegate#application(_:didDiscardSceneSessions:)](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/3197906-application)
・シーンが切断され、セッションが破棄されます。
・ユーザーにより明示的に削除が実行されたので、テキストの下書きのようなユーザーデータはこのタイミングで削除できます。

## 新しいアプリではベストプラクティスとして推奨

|Deprecated|Added|
|---|---|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/904cf91b-eb42-898b-4f9f-1108577f6bb3.png" width="600"><br>[UIApplication](https://developer.apple.com/documentation/uikit/uiapplication)|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/f45114dc-1a61-a857-f2ee-9fa346afd518.png" width="600"><br>[UIWindowScene](https://developer.apple.com/documentation/uikit/uiwindowscene)|

iOS13では、UIApplication、UIApplicationDelegate からUIの状態とプロセスのライフサイクルの責任が分離されました。
これに伴い`UIStatusBar`、`UIWindow`は`UIApplication`の管理するところではなくなり、これらのメソッドやプロパティは`UIApplication`から非推奨となっています。非推奨となったのUIの状態に関するメソッドは、`UIWindowScene`を使って置き換えられます。マルチウインドウを使う予定がなくても、今後マルチウインドウにしたい時に役に立つので、新しいプロパティを採用することがお勧め、とのことです。

例えば、シーンごとに、ステータスバーの色をライトモードまたはダークモードに表示できます。
Xcode11では、この`UIScene API`のライフサイクルを使用したものが、デフォルトテンプレートとなっています。

## プログラムからのシーンの作成・更新・破棄

引数となるセッションは、UIViewを介しても取得することが出来ます。

```Swift:UIViewからUISceneSessionを取得する
   let session = view.window?.windowScene?.session
```
シーンの作成です。

```Swift:シーンの作成
   // (A) 既存セッションから作成
   UIApplication.shared.requestSceneSessionActivation(session, userActivity: nil, options: nil)

   // (B) 新規に作成（必要に応じてアクティビティをセット）
   let activity = NSUserActivity(activityType: "com.example.MyApp.EditDocument")
   activity.userInfo["url"] = url
   UIApplication.shared.requestSceneSessionActivation(session, userActivity: activity, options: nil) 
```

シーンの更新。これを行うとUIが更新されたスナップショットがAPPスイッチャーに保存されます。

```Swift:シーンの更新
   UIApplication.shared.requestSceneSessionRefresh(session)
```
シーンの破棄では、シーンを閉じる時のアニメーションを選択することが出来ます。

```Swift:シーンの破棄
   let options = UIWindowScene.DestructionRequestOptions()
   options.windowDismissalAnimation = .standard // シーンを閉じる時のアニメーションを選べる
   UIApplication.shared.requestSceneSessionDestruction(session, options: options)
```

- シーン作成：[UIApplication#requestSceneSessionActivation(_:userActivity:options:errorHandler:)](https://developer.apple.com/documentation/uikit/uiapplication/3197901-requestscenesessionactivation) 
- シーン更新：[UIApplication#requestSceneSessionRefresh(_:)](https://developer.apple.com/documentation/uikit/uiapplication/3197903-requestscenesessionrefresh) 
- シーン破棄：[UIApplication#requestSceneSessionDestruction(_:options:errorHandler:)](https://developer.apple.com/documentation/uikit/uiapplication/3197902-requestscenesessiondestruction) 

## マルチシーンの実践・デバッグ

#### ステートが共有されたことによる不具合の２つの事例

##### その1：シーンの非同期（区別）
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/182ff4a8-bcc5-ba58-7784-ca01efcc6466.png" width="600">

これらのようなオブジェクトはシングルトンとして、またはそれに近い形で利用される可能性があり、アプリの中でよく使用されます。

問題：マルチシーンでこのようなオブジェクトを扱う際、別々の処理内容として扱うべきであるのに、同時に一箇所にデータを書き込むようなことが発生しがちです。
例えば、テキストエディタアプリで、これまで編集中の内容は１つのファイルとして保存していたところ、マルチシーンでは、他方の内容がもう一方の内容を上書きしてしまい、不整合がおきました。

解決：この場合、シーン（セッション）ごとの編集中のデータが、別々のファイルに保存されるようにします。`UISceneSession#persistentIdentifier`を編集中のデータに加え、シーンと関連付けるIdとして利用できます。シーンごとにステートを区別出来るようにしましょう。

|Before|After|
|---|---|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/07de4241-e528-858e-2436-645c4be42384.png">|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/ea4ee772-cc65-f8ba-6f50-d4a7b63f8154.png">|

シーン再接続の際に`scene(_:willConnectTo:options:)`で、マニュアルでの状態復元データとして使用できます。シーンの状態復元が終わったら、この復元用データは忘れずにクリーンアップしましょう。
また、シーンのライフサイクルと関連づいた復元用データは[application(_:didDiscardSceneSessions:)](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/3197906-application)で削除するのが便利です。ユーザーがシーンを破棄すると呼ばれます。

##### その２：シーンの同期

問題：すべてのシーン間で共有されるべきUIの設定変更が、変更を実行したシーンにしか反映されていません。
（下のバーが左側のシーンにしか表示されていない↓）

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/c22aaf02-af8c-1a93-6cc1-469d6d52d517.png">

解決：シーン間で共有されるべき設定値は、KVO(Key-Value Observing)で共有するのが洗練された方法です。UserDefaultsを拡張し、コンピューテッドプロパティにラップして、`\UserDefaults.isInfoBarHidden`といったKVO用のKeyPathを得られるようにしておきます。vc側では変更を監視するようにします。この時、observe時のoptionには`.initial`を指定しておけば、変更の有無にかかわらず`viewDidLoad()`で一度実行されるため、UI変更を実行するコードも一箇所で済みます。

```Swift:UserDefaultsの拡張
// Add a Key-Value Observable Property to UserDefaults
extension UserDefaults {
    private static let isInfoBarHiddenKey = "IsInfoBarHidden"
    @objc dynamic var isInfoBarHidden: Bool {
         get { return bool(forKey: UserDefaults.isInfoBarHiddenKey) }
         set { set(newValue, forKey: UserDefaults.isInfoBarHiddenKey) }
    }
}
```
```Swift:vc側での変更監視
    class DocumentViewController: UIViewController {
    private var observer: NSKeyValueObservation?
    override func viewDidLoad() {
        observer = UserDefaults.standard.observe(\UserDefaults.isInfoBarHidden,
options: .initial, changeHandler: { [weak self] (_, _) in
              // 変更内容
              let controller = self?.navigationController?
              controller.isToolbarHidden = UserDefaults.standard.isInfoBarHidden
        })
     }
}
```

## NotificationCenterを利用したシーンの同期

前段でUserDefaultsをKVOしてシーンの同期を行う事例でしたが、NotificationCenterを用いてモデルコントローラから変更通知できるようにした事例が、[別の動画](https://developer.apple.com/videos/play/wwdc2019/258/?time=600)で紹介されていたため要約を記載します。

**問題：**
チャットアプリで、iOS13でマルチウインドウのサポートしました。
いま、同じ会話をSplit Viewで左右に並べて２つ、同時に表示しています。
相手にメッセージを送信したところ、更新されたのは操作を行ったシーンのみでした。（両方とも更新されるべき。）

このアプリでは、チャットでの1発言を`Message`モデルとし`ChatModelController（シングルトン）`で管理を行っています。
`ChatViewController`が送信ボタンのタップイベントを受け取り、チャットにメッセージを追加（ビューの更新）した後、モデルコントローラーにMessageの保存を依頼しています。UIインスタンスが１つなら問題ありませんでした。

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/247a8806-a11c-0c90-bfca-f0d1a7899c1d.png">

```Swift:Before　このシーンのvc
class ChatViewController: UIViewController {
    @objc func didEnterMessage(sender: UITextField) {
        let message = Message(text: sender.text)
        // (1)自分のUIしか更新されなかった
        // Update views 
        self.animateNewRow(for: message)
        self.updateBadgeCount()
        // Update the model
        ChatModelController.shared.add(message: message)
    }
}
```

**解決：**
ビューの更新を他のシーン（UIインスタンス）へも通知する必要があります。
モデルコントローラーにNotificationCenterでの実装を施し、データ更新を監視する全てのシーンへ通知する仕組みに変更しました。

|Before|After|
|---|---|
|<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/b64914f2-e5ab-7135-9197-770b035e62dc.png">|<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/554b8845-82af-f48b-1c23-fa3f08979f0e.png">|

新しい型を作成し、それを更新イベントと呼びます。

```Swift:After
// (2)
enum UpdateEvent {
    case NewMessage(message: Message)
    static let NewMessageNotificationName = Notification.Name(rawValue: "NewMessage")
    func post() {
        // Notify subscribers
        switch self {
        case .NewMessage(message: _):
            NotificationCenter.default.post(name: UpdateEvent.NewMessageNotificationName, object: self)
        }
    }
}
```
(2)`UpdateEvent`型を作成し、新着メッセージを付属型のenumのcaseとして定義しました。
また、`NotificationCenter`を実装に利用した`post()`を備え、自身の値を監視するオブジェクトへ通知できるようにしています。

```Swift:After
// (3)
class ChatModelController {
    static let shared = ChatModelController()
    func add(message: Message) {
        saveToDisk(message)
        let event = UpdateEvent.NewMessage(message: message)
        event.post()
    }
}
```
(3)モデルコントローラの`add(message:)`には、これまでのMessageの永続化処理に加えて、更新イベントenumのインスタンス化と`post()`の実行を行うようにしました。

```Swift:
class ChatViewController: UIViewController {
    @objc func didEnterMessage(sender: UITextField) {
        let message = Message(text: sender.text)
        // （(1)の処理はこの場所から削除）
        // Update the model
        ChatModelController.shared.add(message: message)
    }
    override func viewDidLoad() { 
        // (4)
        NotificationCenter.default.addObserver(self, selector: #selector(handle(notification:)), name: UpdateEvent.NewMessageNotificationName, object: nil)
    }
    // (5)
    @objc func handle(notification: Notification) {
        let event = notification.object as! UpdateEvent
        switch event {
        case .NewMessage(let newMessage):
            // (1)のコードはここへ移動された
            // Update the UI
            self.animateNewRow(for: newMessage)
            self.updateBadgeCount()
        }
    }
}
```
(4)`viewDidLoad()`でイベント監視の登録を行うようにします。モデルコントローラからの通知を受け取ることができるようになりました。
(5)`handle(notification: Notification)`は通知を受けとるハンドラメソッドです。
`UpdateEvent`を列挙型(enum)にし通知オブジェクトとして利用したことにより、イベントの種類(case)によるSwitch処理も簡単で、`Message`もオブジェクトから引き出すことができます。
(1)の処理はここへ移動しました。
これですべてのシーンが更新されるようになりました。（めでたし）

## 参考リンク
- [Scenes](https://developer.apple.com/documentation/uikit/app_and_environment/scenes)
- [Introducing Multiple Windows on iPad](https://developer.apple.com/videos/play/wwdc2019/212/)
- [Architecting Your App for Multiple Windows](https://developer.apple.com/videos/play/wwdc2019/258/)
- [Targeting Content with Multiple Windows](https://developer.apple.com/videos/play/wwdc2019/259/)

***
[^1]:1. 例外的にSafariのみ、iOS12のiPadでマルチウインドウ出来たようです。