iOS 13からのダークモード対応のコツ

iOS 13からのダークモード対応について、具体的な実装方法、注意点、現行バージョンと並行開発するコツなどをまとめてみました。

## 定義済みのダイナミックシステムカラーについて

これまで色の指定は `UIColor.white` のように静的な色を使っていましたが、iOS 13からはライト・ダークのモードに応じて変化するダイナミックカラーという仕組みが導入されました。

システムで定義されたダイナミックカラーを `UIColor` から利用することができます。`label`, `systemBackground` のように用途に応じた名前で定義されるものはセマンティックカラーとも呼ばれます。システムと同じ色を使うのが適当なケースではこれらを利用しましょう。

## ダークモードかどうかを判定する

ダークモードかどうかは `UITraitCollection` のプロパティ `userInterfaceStyle` で判定できます。 `UIView` や `UIViewController` は `traitCollection` プロパティから `UITraitCollection` のインスタンスにアクセスできます。

また、以下のようなエクステンションを書いておくと便利だと思います。 availability の判定をあちこちに書かずに済ませるためにも有効です。

```swift
extension UITraitCollection {

    public static var isDarkMode: Bool {
        if #available(iOS 13, *), current.userInterfaceStyle == .dark {
            return true
        }
        return false
    }

}
```

ただし、以下の項目で説明しますが、実際にこのコードを利用するシーンはあまりないと思います。

## ダイナミックカラーを生成する

ビューの生成時などにダークモードかどうかを判定して静的な色をセットしても、アプリ動作中にモードが切り替わったときに追随できなくなってしまいます。

モード切り替え時に追随できるようにするにはダイナミックカラーを利用します。 `UIColor` にクロージャーを渡して、クロージャーの中でモードごとの色を返すようにすることで、動的な色を作れるイニシャライザが新たに導入されました。 `UIColor` が直接利用できる場面ではこれを使うのがいいでしょう。

```swift
 let dynamicColor = UIColor { (traitCollection: UITraitCollection) -> UIColor in
    if traitCollection.userInterfaceStyle == .dark {
        return .black
    } else {
        return .white
    }
}
```

こちらも以下のようなメソッドを `UIColor` のエクステンションに書いておくと便利です。

```swift
/// ライト/ダーク用の色を受け取ってDynamic Colorを作って返す
public class func dynamicColor(light: UIColor, dark: UIColor) -> UIColor {
    if #available(iOS 13, *) {
        return UIColor { (traitCollection) -> UIColor in
            if traitCollection.userInterfaceStyle == .dark {
                return dark
            } else {
                return light
            }
        }
    }
    return light
}
```

## カスタムのセマンティックカラーを定義する

アプリ独自のカラーセットをセマンティックカラーとして `UIColor` のエクステンションに定義して、実際に色を指定する箇所ではこちらを利用するのがいいと思います。ダークモード導入以前からこのように定義しているアプリも多いのではないでしょうか。

ダイナミックカラーを返すようにすることで、モード切り替え時も自動的に色が置き換わるようになります。

```swift
/// 背景色
public static var background: UIColor {
    return dynamicColor(
        light: .white
        dark: .black
    )
}

/// メインのテキストの色
public static var text: UIColor {
    return dynamicColor(
        light: UIColor(hex: 0x212121),
        dark: UIColor(hex: 0xF5F5F5)
    )
}
```

※ `init(hex:)`も独自のエクステンションです。

deployment targetがiOS 11以上であればAsset CatalogでColor Setを定義することができます（画像はMacアプリの例ですがiOSも同様です）。右ペインのAppearacesから「Any, Dark」を選択するとダークモード用の色を定義できます。

![Asset CatalogでColor Setを定義する](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32084/67cf9874-fb3e-a702-d27c-40ee1546a659.png)
[Supporting Dark Mode in Your Interface](https://developer.apple.com/documentation/appkit/supporting_dark_mode_in_your_interface) より

Storyboardから指定したり、コードから `init(named:)` で指定すればダイナミックカラーとして利用できます。

## ダイナミックカラーを直接使えないケース

色指定に `CGColor` を使わないといけないケースや、 `withAlphaComponent(_:)` でalphaをかけているケースなどは、ダイナミックカラーを直接利用できないので注意が必要です。

このようなケースでは、モード切り替え時に呼ばれるメソッドの中で色の指定をしておくことで、適切にアップデートできます。モード切り替え時に呼ばれるメソッドは以下の通りです。

- `UIView`
  - `traitCollectionDidChange(_:)`
  - `layoutSubviews()`
  - `draw(_:)`
  - `updateConstraints()`
  - `tintColorDidChange()`
- `UIViewController`
    - `traitCollectionDidChange(_:)`
    - `updateViewConstraints()`
    - `viewWillLayoutSubviews()`
    - `viewDidLayoutSubviews()`
- `UIPresentationController`
    - `traitCollectionDidChange(_:)`
    - `containerViewWillLayoutSubviews()`
    - `containerViewDidLayoutSubviews()`

`CGColor` で色を指定する例

```swift
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    textLabel.layer.borderColor = UIColor.border.cgColor
}
```

## 現行バージョン向けと並行開発する

iOS 13のリリースまでまだ時間がありますので、それまでに新規の画面開発や既存画面の大幅な改修などが入ることもあると思います。その場合、今のうちにダークモード対応のことを考慮しておきたいので、現行バージョンのXcode 10用ブランチとiOS 13対応をしているXcode 11用ブランチで同一のコードが動く状態にしました。

例えば上に挙げたdynamic colorを返すメソッドは、以下のように `#if swift(>=5.1)` を挟むことによってどちらの環境でもビルドして動作させることができます。

```swift
public class func dynamicColor(light: UIColor, dark: UIColor) -> UIColor {
    #if swift(>=5.1)
    if #available(iOS 13, *) {
        return UIColor { (traitCollection) -> UIColor in
            if traitCollection.userInterfaceStyle == .dark {
                return dark
            } else {
                return light
            }
        }
    }
    #endif
    return light
}
```

## 参考リンク

- [WWDC 2019 - Implementing Dark Mode on iOS](https://developer.apple.com/videos/play/wwdc2019/214/)
- [Human Interface Guidelines - Color](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/color/) 
- [Human Interface Guidelines - Dark Mode](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/dark-mode/) 
- [Supporting Dark Mode in Your Interface](https://developer.apple.com/documentation/appkit/supporting_dark_mode_in_your_interface)