LaunchScreenを表示したままにしてViewControllerを動かす

# やりたいこと
![S12019-09-17 6.11.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95033/36f1101f-b91a-7e3e-3c6a-977ed1aadfc3.png)

LaunchScreenを表示した後、**画面はそのままで**初期化用のStartupViewControllerを起動します。
初期化終了後にMainViewControllerを起動します。

# StartupViewControllerを起動する

SceneDelegateのscene(_: willConnectTo:options)でStartupViewControllerをrootViewControllerにセットします。

```SceneDelegate.swift
// root view controller
let viewController = StartupViewController()
window.rootViewController = viewController
window.makeKeyAndVisible()
```

# LaunchScreenのStoryboard名をInfo.plistから取得する
ハードコードで"LaunchScreen"でもいいんですけど。:relaxed:

```InfoPlistUtil.swift
struct InfoPlistUtil {

    private init() {} // prevent instantiation.

    static func objectForKey<T>(_ key: String) -> T? {
        Bundle.main.object(forInfoDictionaryKey: key) as? T
    }

    static func stringForKey(_ key: String) -> String? {
        objectForKey(key)
    }
        
    static var launchStoryboardName: String? {
        stringForKey("UILaunchStoryboardName")
    }
}
```

# LaunchScreenを取得する
LaunchScreenをViewControllerごとインスタンス化してそのviewを取得します。

```ViewUtil.swift
struct ViewUtil {

    private init() {} // prevent instantiation.

    static var launchScreen: UIView? {
        guard let name = InfoPlistUtil.launchStoryboardName else {
            return nil
        }
        let storyboard = UIStoryboard(name: name, bundle: nil)
        let viewController = storyboard.instantiateInitialViewController()
        let retainedView = viewController?.view // retain Launch Screen
        viewController?.view = nil // Cut reference from viewController to Launch Screen
        return retainedView
    }
}
```
肝なところなので[解説](https://qiita.com/Hackenbacker/items/8abcdbb77c42f3670749#解説)もご覧ください。

# StartupViewControllerにLaunchScreenをセットする
StartupViewControllerのviewにLaunchScreenをセットします。


```StartupViewController.swift
override func loadView() {
    super.loadView()

    if let launchScreen = ViewUtil.launchScreen {
        view = launchScreen
    }
}
```

# MainViewControllerを起動する
初期化終了後にMainViewControllerをインスタンス化してrootViewControllerにセットします。

このまま実行できませんが雰囲気はわかりますよね？ベタで書くの面倒だったので。:relaxed:

```StartupViewController.swift
// 初期化終了後
let viewController = ViewUtil.retrieveViewController(MainViewController.self)
RootViewControllerSegue(source: self, destination: viewController)
    .perform()
```

# 解説
## ダメパターン(1)
![001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95033/c9694471-38a1-9eca-0232-503767883e70.png)
instantiateInitialViewController()を実行した直後はLaunchScreenが無名ViewControllerに割り当てられています。
この状態でLaunchScreenをStartupViewControllerに割り当てるとクラッシュします。
(1つのUIViewを複数のUIViewControllerに割り当てできないため)
## ダメパターン(2)
![002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95033/67099c6d-0e41-5ee8-c344-1fd85ef35387.png)
無名ViewControllerからLaunchScreenへの参照を切ってみたらどうでしょうか？
LaunchScreenの参照数が0になってしまうので、LaunchScreenがメモリから解放されてしまいます。
(詳細はARCの仕組みを調べてみてください)
## 成功パターン
![003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95033/5e3dd77e-081a-2e91-d05f-69c42140e4a5.png)
1. 変数retainedViewを用意してLaunchScreenを参照することで参照数が0になることを防ぎます。
2. 無名ViewControllerからLaunchScreenへの参照を切ります。
3. LaunchScreenをStartupViewControllerに割り当てます。

# ソースコード
GitHubにソースコード一式を置きましたの参考にしていただければ幸いです。
[hackenbacker/LaunchScreenSample](https://github.com/hackenbacker/LaunchScreenSample)



---
# 動作環境
Xcode13.4.1 + iOS13.0 or later