[iOS][Swift]UIColorからUIImageを生成する(ダークモード対応版)



# 概要
参考 https://qiita.com/akatsuki174/items/c0b8b5126b6c12f62001

参考リンクに挙げた記事の通り`UIKit`の`UIButton`のハイライト時の背景色を指定したい場合など、色から指定したいが仕様上`UIImage`でしか設定することができないという状況では、`UIColor`から`UIImage`を生成する必要があります。
iOS12までは参考リンクの通りで良いのですが、iOS13ではダークモードが搭載されたため特定のケースで上手く動かなくなることがあります。
本記事ではダークモードに対応したUIColorからUIImageを生成する方法を紹介していきます。

# 方法
先に方法だけ提示します。解説は以下の項に続きます。

```swift
extension UIImage {
    static func filledImage(byColor color: UIColor) -> UIImage {
        let createImage = { (rawColor: UIColor) -> UIImage in
            let rect = CGRect(x: 0, y: 0, width: 1, height: 1)
            UIGraphicsBeginImageContext(rect.size)
            let context = UIGraphicsGetCurrentContext()!
            context.setFillColor(rawColor.cgColor)
            context.fill(rect)
            let image = UIGraphicsGetImageFromCurrentImageContext()!
            UIGraphicsEndImageContext()
            return image
        }

        if #available(iOS 13.0, *) { //ダークモードはiOS13からなので分岐する必要がある
            let image = UIImage()
            let appearances: [UIUserInterfaceStyle] = [.light, .dark]
            appearances.forEach {
                let traitCollection = UITraitCollection(userInterfaceStyle: $0)
                image.imageAsset?.register(createImage(color.resolvedColor(with: traitCollection)),
                                           with: traitCollection) // ライトモードとダークモードの色を直接指定してImageを生成している
            }
            return image
        } else {
            return createImage(color)
        }
    }
}

extension UIColor {
    var image: UIImage {
        UIImage.filledImage(byColor: self)
    }
}
```

# 実装
まず従来の手順で実装してみましょう。
以下のような、普通のボタンと反転した見た目のボタンを表示する機能を実装します。
- 文字色が`UIColor.systemBackground`
- 通常時の背景色が`UIColor.label`
- ハイライト時の背景色が`UIColor.secondaryLabel`

## 従来の手順

XCodeで`Single View App`を選択しプロジェクトを作成したら、まず`UIColor`から`UIImage`を生成するExtensionを実装します。

```swift
//  UIImage+Color.swift

import UIKit

extension UIImage {
    static func filledImage(byColor color: UIColor) -> UIImage {
        let rect = CGRect(x: 0, y: 0, width: 1, height: 1)
        UIGraphicsBeginImageContext(rect.size)
        let context = UIGraphicsGetCurrentContext()!
        context.setFillColor(color.cgColor)
        context.fill(rect)
        let image = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        return image
    }
}

extension UIColor {
    var image: UIImage {
        UIImage.filledImage(byColor: self)
    }
}
```

これを利用し、適当にStoryBoard上で`UIButton`を中央に置いた`UIViewController`に対してボタン色を設定します。

```swift
//  ViewController.swift

import UIKit

class ViewController: UIViewController {
    @IBOutlet var button: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        button.setTitleColor(.systemBackground, for: .normal)
        button.setBackgroundImage(UIColor.label.image, for: .normal)
        button.setBackgroundImage(UIColor.secondaryLabel.image, for: .normal)
    }
}
```

これをライトモード、ダークモードでそれぞれアプリを起動すると以下のような表示になります。上手く動いているように見えますね。

| Light | Dark |
| --- | --- |
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16785/48ec7029-7088-d17b-39d6-3aeb3e61c1c5.png" width=320> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16785/fc807155-b4a3-aed9-9ad9-0bebe308e6d0.png" width=320> |

では、この`UIViewController`を表示したままiOSの設定を変更し、ライトモード/ダークモードを切り替えるとどうなるでしょうか？

| Light -> Dark | Dark -> Light |
| --- | --- |
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16785/dff2cb6d-cf39-28a3-96fb-2a06af4edb10.png" width=320> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16785/bfcfba2f-c6e2-4090-c095-5df22af401e1.png" width=320> |

様子がおかしいですね。画面の背景色やボタンのテキストはモード変更に追従しているのにボタンの背景色だけが追従できていないようです。
なぜなら`UIButton`の背景色を設定した段階の`UIImage`で固定されてしまうためです。
iOS12まで通用していた方法では、画面の表示後に表示モードを切り替えられると不具合が発生してしまうのです。

## 対策の手がかり
### UIImage.imageAsset
ライトモード/ダークモード変更に対処するための仕組みが`UIImage`には有り、以下のような形で利用することができます。
`UIImage.imageAsset`を利用するとそれぞれのモードに設定された適切な画像を自動で選択し、表示に反映します。

```swift
let image = UIImage()
image.imageAsset?.register(UIImage(named: "light.png")!,
                                   with: UITraitCollection(userInterfaceStyle: UIUserInterfaceStyle.light))
image.imageAsset?.register(UIImage(named: "dark.png")!,
                                   with: UITraitCollection(userInterfaceStyle: UIUserInterfaceStyle.dark))
```


### UIColor.resolvedColor
`UIColor`はモード別の色を内包したクラスですが、`UIColor.resolvedColor`を利用すると特定のモードの色を直接取り出すことが可能です。

``` swift
let color = UIColor.label
let lightColor = color.resolvedColor(with: UITraitCollection(userInterfaceStyle: UIUserInterfaceStyle.light))
let darkColor = color.resolvedColor(with: UITraitCollection(userInterfaceStyle: UIUserInterfaceStyle.dark))
```

## 解決

前述の内容を踏まえて冒頭に紹介したとおりにExtensionを書き換えてみましょう。

```swift
//  UIImage+Color.swift
extension UIImage {
    static func filledImage(byColor color: UIColor) -> UIImage {
        let createImage = { (rawColor: UIColor) -> UIImage in
            let rect = CGRect(x: 0, y: 0, width: 1, height: 1)
            UIGraphicsBeginImageContext(rect.size)
            let context = UIGraphicsGetCurrentContext()!
            context.setFillColor(rawColor.cgColor)
            context.fill(rect)
            let image = UIGraphicsGetImageFromCurrentImageContext()!
            UIGraphicsEndImageContext()
            return image
        }

        if #available(iOS 13.0, *) { //ダークモードはiOS13からなので分岐する必要がある
            let image = UIImage()
            let appearances: [UIUserInterfaceStyle] = [.light, .dark]
            appearances.forEach {
                let traitCollection = UITraitCollection(userInterfaceStyle: $0)
                image.imageAsset?.register(createImage(color.resolvedColor(with: traitCollection)),
                                           with: traitCollection) // ライトモードとダークモードの色を直接指定してImageを生成している
            }
            return image
        } else {
            return createImage(color)
        }
    }
}

extension UIColor {
    var image: UIImage {
        UIImage.filledImage(byColor: self)
    }
}
```

この実装をビルドしてアプリを起動し、`UIViewController`を表示したままiOSの設定を変更し、ライトモード/ダークモードを切り替えてみましょう。

| Light -> Dark | Dark -> Light |
| --- | --- |
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16785/adf1262a-6fd0-9a73-d26e-337d448bf37d.png" width=320> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16785/ae59b8ce-90a9-dfb8-847a-95f0fa9eb56c.png" width=320> |

どうでしょう。ボタンの背景色がモード変更に追従できているようです。
今回紹介した方法を使えば、`UIColor`から生成した単色の`UIImage`もダークモード対応できるようになるわけです。よかったですね。