【iOS 13】アプリアイコンをダークモード対応させることは可能なのか検証してみる!!

## はじめに

皆さんはダークモード対応されていますか？
だいぶ対応したアプリが増えてきたように感じます。
対応するのは大変(主に工数が)ですがやっぱり対応したら謎の満足感あります。

私は個人開発のアプリでは対応しましたが，
まだ業務で担当している案件では対応できていないです。

Xcode では，画像や色のアセットでライト/ダークモードで
それぞれ使う色，画像を設定できるのですごく楽ですよね。

**アプリアイコンはダークモード対応できないのかな🤔？**
と思って調べてみました。

ちなみにアプリアイコンは Xcode にダークモードなどの設定はないですね。
<img width="261" alt="01_img_AppIcon_no_settings.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/b25ae497-63ac-8c67-ab6e-4f7b2ff54eb6.png">

YUMEMI.swift #5 でトークした内容になります。
https://yumemi.connpass.com/event/153206/

## アプリアイコンを変更する(iOS 10.3 ~)

アプリアイコンの変更や名前取得などは iOS 10.3 から使えます。

```swift
// hogeという名前のアプリアイコンを設定する
if #available(iOS 10.3, *) {
    UIApplication.shared.setAlternateIconName("hoge") { error in
        if let error = error {
            print(error.localizedDescription)
        }
    }
}
```

元のアイコンに戻すときは nil をセットすれば良い。

```swift
// 元のアイコンに戻す
if #available(iOS 10.3, *) {
    UIApplication.shared.setAlternateIconName(nil) { error in
        if let error = error {
            print(error.localizedDescription)
        }
    }
}
```

注意点として下記があります。

* `info.plist` に利用するアイコン情報を記載する
* `ViewController` に記載する
* メインスレッドで実行する
* 変更するアプリアイコンは `xcassets` を利用せず直接導入する

これらのコードとライト/ダークモードが切り替わった際に呼ばれる，
メソッドを `override` して，**Appearance** が切り替わった際に
それぞれのモードに対応したアプリアイコンを変更させようとしてみます。

## 実装

### 実装環境

* **Xcode 11.2.1**
* **macOS Catalina 10.15.1**
* **iOS 13 and later**

サンプルコードは GitHub に用意しました。
必要に応じて参照ください。
[https://github.com/MilanistaDev/AppIconCorrespondingToDarkMode](https://github.com/MilanistaDev/AppIconCorrespondingToDarkMode)
![QRCode_APPICON_Darkmode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/d2b44f51-a0a4-eeb3-73ba-18e1ef8b0d9a.png)

### 本実装

#### ライトモードで設定するアプリアイコン画像をセット

ライトモードで設定するアプリアイコンを `xcassets` の **AppIcon** で設定します。
いわゆるいつものアプリアイコンセット作業です。
<img width="496" alt="02_img_AppIcon.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/cf41aeff-0115-8d1b-a47e-883bcc54aa8b.png">

#### ダークモードで設定するアプリアイコン画像を準備

ダークモード用アプリアイコンの画像を用意します。
`@2x`，`@3x` でそれぞれ **120x120**，**180x180** ピクセルです。
名前は **AppIcon-Dark** としました。
<img width="797" alt="03_img_darkmode_icons.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/37668c40-1027-f772-53e5-fc6b98fd063c.png">

#### 用意した画像をプロジェクトに追加

先にあった通り `Assets.xcassets` で AppIcon を追加しても
ダークモード用の設定ができません。
なので直接プロジェクトに追加します。
<img width="400" alt="04_img_AddedDakModeAppIcon.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/60340160-4c36-65fe-2ca3-d1b177474e5a.png">

#### info.plist を編集

追加した画像を利用するために `info.plist` を編集する必要があります。
下記コードを追加します。

```xml
<key>CFBundleIcons</key> 
<dict> 
    <key>CFBundlePrimaryIcon</key> 
    <dict> 
        <key>CFBundleIconFiles</key> 
        <array> 
            <string>AppIcon</string> 
        </array> 
        <key>UIPrerenderedIcon</key> 
        <false/> 
    </dict> 
    <key>CFBundleAlternateIcons</key> 
    <dict> 
        <key>AppIcon-Dark</key> 
        <dict> 
            <key>CFBundleIconFiles</key> 
            <array> 
                <string>AppIcon-Dark</string> 
            </array> 
            <key>UIPrerenderedIcon</key> 
            <false/> 
        </dict> 
    </dict> 
</dict>
```

見やすいプロパティリストで見ると下記のようになります。
**Primary Icon** の方がライトモードのアプリアイコン設定で，
黄色の四角の部分が追加したダークモード用アプリアイコン画像のファイル名です。
<img width="415" alt="05_img_infoplist_pl.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/aacf8c3c-3928-d5d3-b4ed-e7eb1d30f733.png">

#### ライト/ダークモードの設定でアプリアイコンを切り替える

`ViewController`でアプリアイコンを変更するコードを書く必要があるので，
例えば `ViewController.swift` に下記のようなコードを書きます。
このコードで Appearance が変更された際に，
現在のモードを判別してアプリのアイコンを切り替える処理が実行されます。

```swift:ViewController.swift
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)

    if self.traitCollection.userInterfaceStyle == .dark {
        // ダークモード用のアプリを設定する
        UIApplication.shared.setAlternateIconName("AppIcon-Dark") { error in
            if let error = error {
                print(error.localizedDescription)
            }
        }
    } else {
        // nilをセットしデフォルトのアプリアイコン画像に変更
        UIApplication.shared.setAlternateIconName(nil) { error in
            if let error = error {
                print(error.localizedDescription)
            }
        }
    }
}
```

が，しかし・・・
コントロールセンターでライト/ダークモードを切り替えると
エラー出力部分を通過することがわかります。

エラー内容は下記です。

> The operation was cancelled.

![RPReplay_Final1574672398.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/126d93ff-03a7-af7c-fdec-17616a8e6abf.gif)

いろいろ調べて遅延実行するとうまくいくとのことで
下記コードで包んであげると・・・

```swift
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
}
```

ブレイクポイント止めるとライト/ダークモード切り替え時に，
ちゃんとアプリアイコン変更ダイアログが表示されます。
しかし，ブレイクポイントを貼らなければ状況は変わりませんでした。

|ライト->ダーク|ダーク->ライト|
|:--:|:--:|
|![RPReplay_Final1574673071.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/98d63155-0afa-78af-89bc-fe35dd404e6c.gif)|![RPReplay_Final1574673262.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/73a36883-c5ff-ba26-1294-3d7d0d8d0e97.gif)|

## 結果

ライトモードとダークモードの変更時にアプリアイコンを変更することはできなさそう。
この変更がユーザのアプリ内でのアクションではなく，
アプリ外の操作(iOSによるもの)のため，おそらくキャンセルされてしまうのかなと考えました。
(ユーザが起こしているアクションには違いないのですが😅)

では，ユーザが起こす，アプリ内のトリガーで今のモードを判別して，
アプリアイコン画像をセットする処理を書くとちゃんと設定できるのかな？


## ユーザが選択可能にする

というわけで，よくある仮の設定画面を用意し，
アプリアイコン画像を現在の Appearance に合わせる，的な
処理を書いてみます。

仮の設定画面はこんな感じで適当に準備しました。
最初のセクションの2番目のセルをタップして
アプリアイコン画像を変える処理を書いてみます。

|ライトモード|ダークモード|
|:--:|:--:|
|![Settings_LIGHT.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/948df8ce-6324-d88d-a8e7-db69191fec2d.png)|![Settings_Dark.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/5e5a9752-b978-8d93-67e8-629725a5374b.png)|

`UITableView` の delegate メソッドのコード。

```swift:SettingsViewController.swift
extension SettingsViewController: UITableViewDelegate {
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        self.tableView.deselectRow(at: indexPath, animated: true)
        if indexPath.section == 0 && indexPath.row == 1 {
            // セルがタップされた際にアプリアイコン画像を変更
            self.matchAppIcon()
        }
    }
}
```

現在の Appearance の状態によってライトモード，ダークモード用の
アプリアイコン画像を設定するコードはこんな感じで書いてみました。

```swift:SettingsViewController.swift

/// Match the App Icon to the current Appearance
private func matchAppIcon() {
    if self.traitCollection.userInterfaceStyle == .dark {
        // ダークモード用のアプリを設定する
        UIApplication.shared.setAlternateIconName("AppIcon-Dark") { error in
            if let error = error {
                print(error.localizedDescription)
            }
        }
    } else {
        // nilをセットしデフォルトのアプリアイコン画像に変更
        UIApplication.shared.setAlternateIconName(nil) { error in
            if let error = error {
                print(error.localizedDescription)
            }
        }
    }
}
```

セルをタップしてみたところ，ダイアログが表示され，
アプリアイコン画像を変更することができました。

|ライトモード|ダークモード|
|:--:|:--:|
|![Success_Light.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/eb7919bb-9e14-e293-78b4-44f986b46771.png)|![Success_Dark.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/16a10550-1045-b71f-c117-f55004e5fc0b.png)|

実際の動きは下記のようになります。
ライトモード，ダークモードに切り替わった際にはアプリアイコンはそのまま，
セルのタップをした際に初めてダイアログが出てアプリアイコンが変更されるという感じです。

|ライト=>ダーク|ダーク=>ライト|
|:--:|:--:|
|![RPReplay_Final1574677144.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/f20985e3-9a3f-d480-1b7c-5dfc7ce45038.gif)|![RPReplay_Final1574677169.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/0031a008-e624-3292-312c-d256d855378f.gif)|

## おわりに

コントロールセンターなどでライトモード，ダークモードを切り替える際に
それぞれのモードに対応したアプリアイコン画像を変更することはできないっぽい。

セルのタップやボタンタップなどユーザが起こすアクションをトリガーとすると，
変更した旨のダイアログが出て，各モードに対応したアプリアイコンに変更することは可能でした。

ライトモード，ダークモード切り替え時にアプリアイコン変更できるぜ，
もっとこうした方がクレバーですよー等ありましたらご教示いただければ幸いです。

ご覧いただきありがとうございました！