アプリにDark Mode をコードで実装する

# 概要

こちらの記事は[fivestars](https://www.fivestars.blog/)のブログの一つの記事を翻訳したものです。

[How To Adopt Dark Mode In Your iOS App](https://www.fivestars.blog/code/ios-dark-mode-how-to.html)

前回のダークモード対応の記事で一つ疑問に思ったことが、Asset Catalogに登録したカラーしかダークモード対応できないのか、と思ったからです。

そのため「dark mode implement in code」とかのキーワードで検索してみたらコードからの実装方法が載っていました。今回の記事はメモとして残しておきたいので[Federico Zanetello氏](https://twitter.com/zntfdr)に依頼してみました。許可頂きましたので載せておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44392/4a44751b-9696-937f-89dc-50413aceeacf.png)


喜べ！ iOS 13にはダークモードが付属しています！

私は自分のアプリに新しいデザインを採用し終えたところです。その過程でいくつかのことを学びました。どのようにできるか見てみましょう。

# Two Steps Back First (最初の2つのステップ)

飛び込む前に、いくつかの注意点があります。
ユーザーに最高のエクスペリエンスを提供するには、この部分をスキップしないことが非常に重要です。 
準備はいいですか？さあ、行こう！

### 必要条件

アプリがダークモードをサポートするには、以下が必要です。

1. Xcode 11でビルドおよびリリースされている

Xcode 10（またはそれ以前）を使用している場合、ダークモードが有効になっているデバイスでも、アプリは常に通常の外観を表示します。

2. iOS 13デバイスで実行する

iOS 13（以降）を実行しているデバイスのみがダークモードをサポートします。iOSの以前のバージョンはデフォルトの外観になります。
デフォルトの外観よりも古いiOSでダークモードを強制することはできません（実際、可能ですが、そうするために多くのカスタムロジックを作成する必要がありますが、それは価値がありません）。

> 別の見方をすると、iOS 13の起動時にアプリがダークモードのサポートで更新されていない場合、iOSシステムの設定に関係なく、アプリは常にデフォルトの外観を表示します。 ユーザーがダークモードを有効にし、アプリがまだサポートしていない場合、アプリはその明るいデフォルトインターフェースでユーザーをフラッシュします！

## Your App Supports Dark Mode Already (アプリはすでにダークモードをサポートしています)

これはクールで怖い部分です。
上記の2つの要件が満たされると、ダークモードが有効になるたびに、UIの外観が自動的に切り替わります。

これは素晴らしいことです。なぜなら、多くの作業を無料で行うことができるからです。
ただし、ダークモードでアプリの見栄えがよくない場合、UIの一部は、おそらくこの新しい外観でも、おそらく明るい光沢のある色で奇妙に見えます。これはユーザーにとって非常に悪い経験になります。これを避けるために読んでください。

# You Can Force One Appearance Over The Other (ある外観を他の外観に強制することができます)

アプリでダークモードの採用を避けたい場合は、「ライトモード」を無期限にドロップしてダークモードを選択するか、単にダークモードの採用を延期し、アプリの`info.plist`に新しいキー`UIUserInterfaceStyle`を追加して`Light`または`Dark`の値を設定します。

> そのための別の方法は、Xcode 10でアプリを出荷し続けることです。

## Don’t Force One Appearance Over The Other (あるものを他のものに強制しないでください)

Appleの言葉を使うと“Only a small subset of apps really should be dark all the time, and those are media-centric or content-creation apps“ということです。インターフェイスの外観を1つだけ提供するという非常に正当な理由がない限り、システム設定を常に尊重してください。どちらを好むかは関係ありません。

> Appleはシステム設定に関係なくユーザーがアプリで使用するモードを選択できるアプリ内設定を使用できます：繰り返しますが、アプリにそうする十分な理由がない限り、最高のエクスペリエンスはシステムモードです。不要な冗長設定を提供しないでください。

# Dark Mode Is Not The Only New Mode (ダークモードだけが新しいモードではありません)

これは、ダークモードほどプレスに影響を与えていないかもしれませんが、iOSには常に2つのモードがあります：defaultとhigh contrast です。`Settings` > `Accessibility` > `Increase Contrast`に移動してiOSデバイスでこれを有効にできます。iOS12を実行している場合は`General `もタップします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44392/ed987e20-cd09-cff1-e788-7ee7ee5e2d71.png)

左と右の画像の両方を簡単に読むことができるかもしれませんが、助けを必要とする人（read: high contrast）は素晴らしいでしょう、他の人は何でも読むことができる唯一の方法です。

これを認識したので、次の4つの外観を処理する必要があることは明らかです。

1. Default
2. Default High Contrast
3. Dark Mode
4. Dark Mode High Contrast

これは、可能な限りデフォルトのUIKit（またはSwiftUI）要素に固執することが本当に重要であるもう1つの理由です。

- そうすれば、これらすべての外観のサポートが無料で得られます。
- 独自の色とUIコンポーネントを作成する場合は、各モード、要素、および要素の状態を自分で管理する必要があります。 この仕事は指数関数的に成長します。

# Let’s Get Started! (始めましょう！)

ここまででおめでとうございます！これからは、新しいインターフェイスの外観をサポートするために今すぐできることについて説明します。

ステップ1：Colors

結局のところ、私たちのアプリは画面に色を投げるだけです。色を正しくすることは、アプリがダークモードに99％備えることを意味します。

## (Dynamic) System Colors

iOS 12まで、`UIColor`は`.red`、`.yellow`などのシンプルな色を提供してきました。これらの色はもう使いたくないでしょう。 これらの色は静的です。つまり、色合いが変わらないことを意味します。

iOS 13およびXcode 11では、AppleはSystem Colorsを導入しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44392/a7e0cf31-0de0-ad0e-d767-b3e0dd3b1d99.png)

左から右へ：`.systemBlue`、`.systemGray`、`.systemGreen`、`.systemIndigo`、`.systemOrange`、`.systemPink`、`.systemPurple`、`.systemRed`、`.systemTeal`、`.systemYellow`となります。

上の図からわかるように、古い静的な色とは異なり、システムの色は動的です。色合いは現在のシステムインターフェイスに適応します。

しかし、それらは新しい色だけではありません！ また、暗い外観と明るい外観の違いがより明確なgrayscale colorsの完全な範囲があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44392/c221bdba-9655-0f5d-ae2d-41c582dbfa3d.png)

左から右へ：`.systemGray`、`.systemGray2`、`.systemGray3`、`.systemGray4`、`.systemGray5`、`.systemGray6`。

これらの動的な色（UIColor.blueの代わりにUIColor.systemBlueなど）を使用することにより、インターフェイスは現在のシステム設定に適した色合いを自動的に選択します。これ以上の作業は不要です。

## (Dynamic) Semantic Colors

Xcode 10以前では、`.lightText`および`.darkText`という名前の2色が提供されていました。
これらは静的であり、どのインターフェースにも適合しないため、これらの使用は現在推奨されていません。代わりに、Xcode 11から、`UIColor.label`、`UIColor.placeholderText`、`UIColor.systemBackground`などの完全に新しいセマンティックカラースイートが提供されます。

シェードを説明する代わりに、これらの色の名前は使用目的に基づいています。ほとんどの場合、システム色のように動的であるため、これらを使用することがほとんどです。

最も重要なのは、semantic colorsにより、アプリが他のシステムと同様の外観になるようにすることです。これらを使用することで、アプリはネイティブに感じられます。これは常にユーザーにとって最高のエクスペリエンスです。

これらの動的な色を使用するほど、ダークモードへの適切な適応が速くなります。

## (Dynamic) Custom Colors: The Assets Catalog

これは常に最後の選択肢であるはずです。

これはあなたと設計チームの両方にとって多くの作業、試行錯誤を必要とする唯一のオプションです。
Appleがあなたのために仕事をします。彼らはこれに非常に才能のあるチームが信じられないほどの時間を投資し、彼らの仕事を信頼して使用します。

免責事項がなくなると、独自の動的色をどのように定義して使用しますか？
iOS 11およびXcode 9以降、アセットカタログに色を追加できます。

ここで、色ごとに暗いバリアントを定義することもできます：
これを行うには、Asset Catalogで色を選択し、属性インスペクター（ショートカットキー：⌘⌥4）を開いて、外観を`None`から`Any, Dark`に設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44392/aee1d11d-e947-f731-f71c-60aacfb25349.png)

この時点で、暗い外観の新しいカラーボックスが表示されます。おめでとうございます！ 最初のダイナミックカラーを作成しました！ 各バリアントを適切な色合いに設定すると、準備完了です。

高コントラストも有効にすることを忘れないでください：

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44392/bacb1dc0-f4e5-e06d-f446-d2963a7186b4.png)

繰り返しますが、システムかセマンティックカラーを可能な限り使用することを強くお勧めします。たとえば、ボタンの場合、カスタムカラーを使用することは、ボタン状態ごとに1つの色を定義することを意味します：`.normal`、`.highlighted`、`.focused`、`.selected`、 `.disabled `。

これは、状態ごと、外観ごとに異なる色を要求することを意味します。計算を行う場合、1つのボタンに（5つの状態* 4つの外観=）20、t-w-e-n-t-y、異なる色合いが必要になりました！

## (Dynamic) Custom Colors In Code

まだiOS 10（Asset CatalogでのColorの宣言をサポートしていない）をターゲットにしている場合、または上記のオプションを使用したくない、または使用できない場合、使用できる最後のオプションはコードで色を定義することです：

```swift:Sample.swift
let dynamicColor = UIColor { (traitCollection: UITraitCollection) -> UIColor in
    switch traitCollection.userInterfaceStyle {
    case
      .unspecified,
      .light: return .white
    case .dark: return .black
    }
}
```

これを行わずに、iOS 10をドロップし、アセットカタログを使用します。

# Step 2: Images

ほとんどすべてのアプリは、UIに画像や記号を表示します。これらの画像にユーザーコンテンツ（プロフィール写真など）が表示されている場合を除き、各モードに1つのimage variantを用意することは非常に良い設計です。方法を見てみましょう。

## SF Symbols

AppleはWWDC19で[SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/)を導入しました。 SF Symbolsは、開発者が独自のアプリで使用できるグリフ（1500以上！）の膨大なコレクションです。

Apple自体は、リマインダー、ニュース、マップなどのすべてのストックアプリでSFシンボルを使用しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44392/614fa1ce-81c5-1fc8-3528-bc5d7d2592bb.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44392/1ca99418-45e4-486f-04ef-d2f3c0c82093.png)

Appleはそれらを使用するだけでなく、そうすることを奨励しています。 Apple の言葉でいうと：

> “The system uses SF Symbols, which automatically look great in Dark Mode, and full-color images that are optimized for both light and dark appearances. Use SF Symbols wherever possible.”

「システムはSFモードを使用します。SFシンボルは、ダークモードで自動的にきれいに表示され、明るい外観と暗い外観の両方に最適化されたフルカラー画像です。 可能な限りSFシンボルを使用してください。」

要約すると、SFシンボルは、アプリですぐに使用できるグリフの集まりです。
UIにグリフがある場合は、無料で入手できる1500以上のグリフのいずれかに置き換えることができるかどうかを確認してください。 任意のサイズで完璧に見えるベクトル画像であることに加えて、それらはシステムに組み込まれています：アプリにそれらをパッケージ化する必要はありません！ アプリの容量を小さくしたいですね！

ストーリーボードを使用している場合、画像名フィールドに正しいグリフ名を入力して、使用するシンボルをXcodeに伝えることができます（[Apple SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/)ガイドラインで利用可能なAppleのSF Symbolsアプリで名前を見つけます）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44392/74ef6b31-ddb3-d2b1-b7ba-71941ef94114.png)


または、新しい`API UIImage（systemName :)`を使用してそれらのいずれかをフェッチできます。

```swift:Sample.swift
UIImage(systemName: "star.fill")
```

おかしいと思ったことはありませんか？公式の[SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/)アプリから使用可能なグリフ（.svg）をいつでもエクスポートし、ニーズに完全に合うように変更できます。

SFシンボルの詳細については、WWDC19セッション[206：SF Symbolsの紹介](https://developer.apple.com/videos/play/wwdc2019/206/)をご覧ください。

どこからでもシンボルのリスト全体にアクセスしたい場合、仲間の開発者ノア・ギルモア（再び！）が彼の新しいウェブサイトsfsymbols.comであなたの背中を持っています。

## Custom Template Images (Glyphs)

SFシンボルと同様に、テンプレート画像は属性インスペクターで`Template Image`として"render as"を選択することにより、Xcodeアセットカタログで定義されたモノクロ画像です。

それらを使用すると、いくつかの利点が得られます。名前を付けるには、無料でダークモードを取得します。

template imagesを使用する場合、UIImageViewの色合いを上記の動的な色のいずれかに設定することを忘れないでください：

```swift:Sample.swift
let glyphImage = UIImage(named: "myGlyph")
let glyphImageView = UIImageView(image: glyphImage)
glyphImageView.tintColor = .systemYellow
```

## Other Images

テンプレート画像や写真などのシンボルではない他のすべての種類の画像については、カスタムカラーの場合と同じ手順を実行できます。アセットカタログで外観を`Any、Dark`に設定し、外観ごとに新しいバリアントをドロップします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44392/8bec0932-91fc-0b4f-688f-accbe5750710.png)

画像に関するその他のヒントについては、[この](https://developer.apple.com/documentation/appkit/images_and_pdf/providing_images_for_different_appearances)Appleガイドを使用してください。


カタログ内のアセットにダークバリアントを追加しても、レトロ互換性が損なわれることはありません。古いiOSバージョンでは、常に「外観」でタグ付けされたアセットが選択されます。

# Step 3: UIVisualEffectViews - Semantic Materials

後ほど翻訳予定

# Step 4: Drawing Attributed Text

属性付きテキストを描画する場合、指定しない場合、`.foregroundColor`プロパティは`.black`に設定されます。
代わりに適切な色に設定します（例：`UIColor.label`）。

```swift:Sample.swift
let textToDraw = "FiveStars.blog"
let attributes: [NSAttributedString.Key: AnyObject] = 
  [.font: UIFont.preferredFont(forTextStyle: .title3),
   .foregroundColor: .label]
textToDraw.draw(at: CGPoint.zero, withAttributes: attributes)
```



# Step 5: Dark Mode in CALayers

`CALayers`は動的な色を理解しません。

このレベルでダイナミックカラーを使用する場合、`view.traitCollection`を呼び出すことで、レイヤーのビューから現在の`UITraitCollection`（`.userInterfaceStyle`のアクティブモードを含む）を取得できます。
取得したら、たとえば次のようにして動的な色を解決できます。

```swift:Sample.swift
let resolvedColor = UIColor.label.resolvedColor(with: traitCollection) 
layer.borderColor = resolvedColor.cgColor 
```

複数の色を解決する必要がある場合、`UITraitCollection`には便利な`performAsCurrent（）`メソッドがあり、それを実行できます。

```swift:Sample.swift
let manyDynamicColors = ...

traitCollection.performAsCurrent {
 for resolvedColor in manyDynamicColors {
   let resolvedCGColor = resolvedColor.cgColor
   ...
 }
}
```

# Step 6: Overriding The System Appearance

システム設定に関係なく、ビューが1つの外観に固執することが理にかなっている場合があります。
iOS 13では、`UIView`、`UIViewController`、および`UIWindow`に、システムの外観をオーバーライドできる新しい`overrideUserInterfaceStyle`プロパティが追加されました。

```swift:Sample.swift
let view = UIView()
// this view will inherit the appearance of its superview

let darkView = UIView()
darkView.overrideUserInterfaceStyle = .dark
// this view (and its subviews) will always be in dark mode
```

`overrideUserInterfaceStyle`は、`UIUserInterfaceStyle`型のenumインスタンスです。 この列挙型のケースは、`.dark`、`.light`、または`.unspecified`のいずれかです。これは、view / viewController / windowがsuperview / parentController / systemからインターフェイスを継承することを示すために使用されます。

このプロパティを設定すると、その特定のview / viewController / windowおよびその「下」にあるものに影響を与えます。

そうすることにより、そのviewとそのすべてのSubViewは、システムの設定ではなく、好みを採用します。

システム設定をリッスンするためにビューに戻る場合は、`overrideUserInterfaceStyle`プロパティを`.unspecified`に戻します。

```swift:Sample.swift
let viewWithCustomAppearance = UIView()
viewWithCustomAppearance.overrideUserInterfaceStyle = .dark
// this view (and its subviews) are now in dark mode

viewWithCustomAppearance.overrideUserInterfaceStyle = .unspecified
// this view (and its subviews) follow the appearance of this view superview
```

# A Deeper Look

アプリがUIのstoryboardに完全に依存している場合、おめでとうございます！ これで、ダークモードを完全にサポートするように設定されました。 これらの人々（🙋🏻‍♂️）の中にいない場合、私たち全員がこの幸運なわけではありません。

# Behind The Scenes: Draw Time

iOSは、描画時に動的なcolors/imagesの正しいtint/imageを選択しますが、「描画時間」は正確にいつですか？

ご存知のように、私たちの見解は生涯のある時点で無効になる可能性があります。
ユーザーが画面を回転させた場合、UIViewがインターフェイスに新しい要素を追加する必要がある場合などです。

これからは、インターフェースの外観が変わるたびにビューも無効になります。
次の方法のいずれかを使用している場合、iOSが正しい色合い/素材/画像を選択することが常に保証されます。

| Class       |    適切なライフサイクルメソッド   |
|:----------------:|:-------------------|
|    UIView       |            draw(_:) <br> layoutSubviews() <br> tintColorDidChange() <br> draw(_:) | 
|      UIViewController       |     viewDidLayoutSubviews()    <br> viewWillLayoutSubviews() <br> traitCollectionDidChange() | 
|      UIPresentationController       |     containerViewWillLayoutSubviews()    <br> containerViewDidLayoutSubviews() <br> traitCollectionDidChange() | 

したがって外観固有のロジックをそれらのいずれかに配置します（ただし、不要な作業を行わないようにしてください！）。

外観固有のコードを`init`、`viewDidLoad`またはその他の場所に置かないでください。
これらのメソッドはインターフェイスの外観が変更された場合や変更された場合に再度トリガーされることはありません。

# Debugging Dark Mode

ダークモードの採用を開始する準備がほぼ整いました！ 最初に知っておくべきことがいくつかあります。

## Storyboards

storyboardと`.xib`ファイルでは、画面をプレビューするデバイスと向きを選択する以外に、インターフェイスの設定を明暗の間で切り替えることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44392/72148d4e-4a31-c362-d7cb-924ce9671f65.png)

何かを変更してからアプリをビルドして実行するのではなく、このトグルを使用してダークモードを採用する方がはるかに高速であることがわかりました。特に作業中の画面が簡単に取得できない場合は、同じことを行うことをお勧めします に。

## Simulator

Xcode 11では、デバッグツールバーに新しいボタンがあります。このボタンを使用すると、環境オーバーライドポップアップにアクセスできます。これにより、特にフォントサイズを変更したり、インターフェイスモードを切り替えたり、ハイコントラストを有効にしたりできます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44392/23c9921b-6b5a-d163-f73c-4382c5a26cb1.png)

これを使用して、シミュレーターで両方のinterfaceをテストします。これはinterfaceの外観を変更する唯一の方法です。
リリース前に実際のデバイスで各画面をテストすることを忘れないでください

# Where To start

ダークモードを採用する準備ができたので次のロードマップをご覧ください。

1. Xcode 11ベータ版をダウンロードしてインストールします（ダァ！）
2. ダークモードを有効にしてアプリをビルドして実行しましょう
3. 発見された明らかな「mistakes」を修正しましょう
4. すべてのアセットにダークバリアントを追加します
5. ダークモードを一度に1画面ずつ調整します。
    - `.xibs`ファイルから開始
    - storyboardに移動する
    - コードに移動
    - すべての画面で繰り返します

6. 属性付きテキストを描画するときは必ずフォアグラウンドキーを設定してください
7. 「Draw time」関数ですべての外観ロジックを移動します

アプリをライトモードでテストすることも忘れないでください。
LaunchScreen storyboardを忘れないでください！。


# Tips

## Design System

アプリでデザインシステムを使用している場合は、おめでとう！ ダークモードを採用すると、文字通り5分かかります（この記事全体を読んだ場合）。

## Assets Catalog: bid farewell to iOS 10

iOS 12の採用は2019年6月時点で85％を超えているため、iOS 10のサポートを中止するのは簡単です。これにより、最終的にアセットカタログから直接カスタムカラーを宣言して使用できるようになります。これによりダークモードの採用が迅速かつ簡単になります。

## Use Storyboards and Xibs

interfaceの外観を切り替えるトグルを使用してstoryboard / Xibsを使用すると、両方の外観で各画面をプレビューできます。アプリを何度も作成して実行し、すべての変更をテストする必要はありません。

## Use SwiftUI

SwiftUIで開発されたものはすべて、ダーク/ライトモードとそのハイコントラストのバリエーションを自動的にサポートします。

# Dark Mode Resources

Appleには、ダークモードの採用に関する複数の優れたリソースがあります。

- Apple Human Interface Guidelinesの[Dark Mode section](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/dark-mode/)から始めて、そこからどこに行くかを選択することをお勧めします（その章のリンクがたくさん！）。
- Appleからのもう1つの興味深いリソースは、あなたの Interfaceでのダークモードのサポートです。これは昨年macOS用に作成されましたがiOSでも非常に有効です。

必見のWWDC19セッションもいくつかあります。

- [Implementing Dark Mode on iOS](https://developer.apple.com/videos/play/wwdc2019/214/)
- [What’s New in iOS Design](https://developer.apple.com/videos/play/wwdc2019/808/)

また、（macOSで）ダークモードを初めて紹介した前年のセッションを見るのもとても良いことです。

- [Introducing Dark Mode](https://developer.apple.com/videos/play/wwdc2018/210/)
- [Advanced Dark Mode sessions](https://developer.apple.com/videos/play/wwdc2018/218/)

# Conclusions

ダークモードは何年も使用されており、最終的には手元にあります。

私は毎日Macでそれを使用していますが、ついにiOSでも使用できるようになったことに非常に興奮しています。
どう思いますか？ すぐにダークモードを使用しますか？ あなたのアプリでそれを採用することは多くの努力を要すると思いますか？

私は個人的にそれがリリースされるのを待つことができません！[Twitterで私に連絡して](https://www.twitter.com/zntfdr)、ダークモードでアプリのスクリーンショットを共有してください！

Thank you for reading and stay tuned for more articles!