遷移とviewヒエラルキーとDark Mode

こんばんは。着色職人Lv.2です。

世の中のiOSエンジニアは以下の2つに分類されます（要出典）。

* お行儀よくアプリを開発してきたので、サクッとDark Mode対応を終わらせることができた人
* 無秩序にviewと色を作成し乱用してきたので、すぐにはDark Mode対応が終わらない残念な人

~~1番目のお行儀のよい人は羨ましい~~
2番目の残念な人はどうすればいいでしょうか？
先に負債を完済してお行儀よい人になってもよいのですが、__Dark Mode対応が終わった箇所から順次解禁__という道がある……かもしれません。

つまりどういうことかというと、
Dark Mode対応が終わっていないviewを全てlight styleに指定し、バージョンアップのタイミングで対応済みviewがあればlight style指定を解除し公開する、ということです。
（全viewへの対応が完了するまでは白黒のまだら模様なアプリを晒すことに……）

というわけで、本記事ではいかにlight styleに強制するかを考えていきます。

# 復習: user interface styleを固定する

Dark Modeになると困るviewには以下のように指定しておく。

```swift
view.overrideUserInterfaceStyle = .light
viewController.overrideUserInterfaceStyle = .light
```

この指定はviewヒエラルキーにおける子viewにも適用される。上位のviewやview controllerを選んで指定すれば、子view全体に対して一発で指定することができる。

|UIUserInterfaceStyle|効果|
|---|---|
|`.light`|自身と子viewをlight styleに強制|
|`.dark`|自身と子viewをdark styleに強制|
|`.unspecified`|親viewの設定に従う(初期値)|

例えば、以下のような設定とviewヒエラルキーになっている場合、 `UIWindow` はDark Modeのオンオフに従ってstyleが変わるが、 `UIViewController` から先はlight styleに強制されて変化しない。

```
** info.plist **
User Interface Style = Automatic

** view hierarchy **
[UIWindow] overrideUserInterfaceStyle = .unspecified
          ↓
          ↓
[UIViewController] overrideUserInterfaceStyle = .light
          ↓
[UIView] overrideUserInterfaceStyle = .unspecified
```

# 本題: 遷移先のview controllerのuser interface styleを固定する

遷移先のコンテンツは全部light styleに固定したい、という場合。
遷移先のview controller全部に直接

```swift
override func viewDidLoad() {
    super.viewDidLoad()
    overrideUserInterfaceStyle = .light
}
```

などと書いてもよいが、ここでは遷移元からまとめて指定する方法を考えてみる。

遷移元のview controllerがlight styleなら遷移先もlight styleになってくれないかな、とか、
遷移先view controllerのstyleをサクッと設定する方法がないかな、とか。
それができれば楽なんだけどなー

## pushしたview controllerのuser interface styleを固定する

遷移後のviewヒエラルキーを見てみる。
<img width="301" alt="Screen Shot 2019-12-16 at 0.34.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84589/4710037a-9aad-bd75-9283-bbe1965c235f.png">
遷移元navigation controllerと遷移先view controller（上の画像ではSecondViewController）には親子関係がある。なので、navigation controllerをlight styleに強制しておけば、遷移先も全てlight styleになる。
view controller間には親子関係がないので、遷移前のview controllerがlight style強制になっていても遷移後のview controllerには影響がない。

こちらのメソッドを使ってもOK。子view controllerのuser interface styleをまとめて指定できる。

```swift
class NavigationController: UINavigationController {
    override func overrideTraitCollection(forChild childViewController: UIViewController) -> UITraitCollection? {
        return .init(userInterfaceStyle: .light)
    }
}
```

### バックスワイプすると……

こんな感じのコードを用意してみる。

* `NavigationController` は子view controllerを全部light styleに強制。
* `ViewController` のボタンをタップすると `SecondViewController` にpush遷移する。
    * 背景色はどちらも `UIColor.systemBackground` 。つまり設定されたstyleに対応した色。

```swift
class NavigationController: UINavigationController {
    override func overrideTraitCollection(forChild childViewController: UIViewController) -> UITraitCollection? {
        return .init(userInterfaceStyle: .light)
    }
}

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
    }

    @IBAction func tappedButton(_ sender: UIButton) {
        self.navigationController?.pushViewController(SecondViewController(), animated: true)
    }
}

class SecondViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
    }
}
```

Dark Mode Onの状態で実行。 `NavigationController` 自体は何も強制されていないので、navigation barはモードに合わせてdark styleになる。一方 `ViewController` は `NavigationController` からの指定によりlight styleとなって白い。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84589/90975a25-02ea-b501-a27d-20447c64b433.png" width=25%>

ボタンをタップして遷移。 `SecondViewController` も同様にlight styleで白。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84589/840f186d-d218-d748-2420-f1cdef1d330e.png" width=25%>

ここからずずずっとスワイプしてバックを仕掛ける。すると、
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84589/530facfb-1e80-13cc-4fdc-430b8fa0c05b.png" width=25%>

突然の黒？？？？
子view controllerから外れてstyle強制がなくなってdarkになる。
ちなみにこの状態からスワイプバック操作をキャンセルしてSecondViewControllerを元の位置に戻しても黒いまま。

## modal遷移した先のuser interface styleを固定する

結論から言うと、modal表示で遷移して出したview controllerのuser interface styleを強制する場合は直接 `overrideUserInterfaceStyle = .light` するしかない。はず。

こちらはmodal遷移した時のviewヒエラルキーの様子。
<img width="403" alt="Screen Shot 2019-12-16 at 0.41.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84589/1db29d87-3881-090c-0784-2d6a0a60b2cf.png">
ご覧のように、遷移元のview controller（NavigationControllerやViewController）と遷移先のview controller（SecondViewController）は親子関係にない。従って、遷移元view controllerに対して何かしても遷移先view controllerには影響しない。

```swift
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        // 自分はlight styleに強制されるが遷移先のview controllerに効力は及ばない
        overrideUserInterfaceStyle = .light
    }

    @IBAction func tappedButton(_ sender:) {
        let vc = SecondViewController()

        // もちろん直接設定すればstyleを強制できる
        vc.overrideUserInterfaceStyle = .light
        present(vc, animated: true, completion: nil)
    }

    // modal遷移先のview controllerは子view controllerではないので効果なし
    override func overrideTraitCollection(forChild childViewController: UIViewController) -> UITraitCollection? {
        return .init(userInterfaceStyle: .light)
    }
}
```

# まとめ

* 安易に `overrideUserInterfaceStyle = .light` しても白くならないことがあるので気をつける
* 負債をためない

【宣伝】技術書典8の2日目に本記事のようなDark Modeのネタ話を売るので絶対に買ってください。