UIButtonの枠線色を動的なUserInterfaceStyleの切り替えに対応させる

# 環境
- Xcode 11.5
- Swift 5.2.4
- iOS 13.0~


# 前口上
表題ではUIButtonとしていますが、UIColorで設定されているtextColorとの比較ができるのでUIButtonを用いているだけで、`UITraitEnvironment`を継承している`UIView`や`UIViewController`で対応可能です。
あと動的という表現が適切かどうかはわかりません。

# 問題点
UIButtonの枠線に色を設定するために、 `layer.borderColor`にCGColorを設定します。
※比較のため、textColorとborderColorには同一のUIColorを用います。

```Swift
button.titleLabel?.textColor = UIColor.label // UIColor
button.layer.borderColor = UIColor.label.cgColor // CGColor
```

しかし、上記の設定だけではUIButtonが**生成された後**、

`UserInterfaceStyle`(以降`style`と表記)を切り替えた場合に枠線のみ色が追従しません。

### Light → Dark(ダークモードで生成した場合も同様)

|Light|Dark|
|:---:|:---:|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/476381/1bc114ba-6cab-3cee-d24a-dce20b589271.png" width=300>|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/476381/16c2d686-ae64-aadb-5d2f-fd1f47fd5c3e.png" width=300>|
※背景ViewのbackgroundColorにはSystemBackgroundColorを適用しています

どうやらCGColorで設定されている場合は、`style`の変化には自動で追従できないようです。

# 対応
`UIView`や`UIViewController`が継承している `UITraitEnvironment` の`traitCollectionDidChange(_:)`をoverrideして対応します。
`style`の切り替えを検知して、都度CGColorを生成して設定し直す処理を加えます。

`traitCollectionDidChange(_:)`に関しては[公式ドキュメント](https://developer.apple.com/documentation/uikit/uitraitenvironment/1623516-traitcollectiondidchange)を参照ください。

# 実装
`style`が切り替わった度に`traitCollectionDidChange(_:)`が実行されます。
メソッド内部で、その時の端末の`style`に合ったCGColorを生成して枠線に設定してあげます。


## UIViewControllerで処理を行う場合

```Swift
class HogeViewController: UIViewController {
    /*
    UI等の設定
    */
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        button.layer.borderColor = UIColor.label.cgColor
    }
}

```

## カスタムクラスを作成して処理を行う場合
以下のカスタムクラスを用いて枠線の色を追従させたいボタンを生成します。

```Swift
class CustomButton: UIButton {
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        layer.borderColor = UIColor.label.cgColor
    }
}

```
### Light → Dark(ダークモードで生成した場合も同様)
|Light|Dark|
|:---:|:---:|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/476381/ed1e6f65-e619-fc9b-d95c-740139929824.png" width=300>|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/476381/b2e984af-54e0-3a38-aaa1-9f67a80d6026.png" width=300>|

# 追加実装
上記の対応だけでも実現はできていますが、`traitCollectionDidChange(_:)`のメソッドは`style`以外の変化に対しても処理が走ってしまいます。

※検知対象である`UITraitCollection`に関しても[公式ドキュメント](https://developer.apple.com/documentation/uikit/uitraitcollection)を参照ください。

そこで、無駄を省くために以下の制御を追加します。

```Swift
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    let currentUserInterfaceStyle = UITraitCollection.current.userInterfaceStyle
    let previousUserInterfaceStyle = previousTraitCollection?.userInterfaceStyle
    guard currentUserInterfaceStyle != previousUserInterfaceStyle else { return }
    button.layer.borderColor = UIColor.label.cgColor
}
```

何かしらのUITraitCollectionが切り替わった後の`style`と、切り替わる前の`style`を比較して、変化がなければ枠線に色を再設定する処理を弾いています。(styleが頻出し過ぎて読みづらい...)

※`previousTraitCollection`には、traitCollectionが切り替わる前の情報が保持されてます。


# 終わりに
基本的にライトモードとダークモードは頻繁に切り替わるものではないと思います。
しかし、対応するに越したことはないかもしれません


## 余談①
今回のbuttonの例で言うと、
storyborad上で枠線の色を`UIColor.label`に設定、シュミレータをダークモードにした状態でbuttonを生成すると、枠線の色はライトモード用の色として生成されてしまいました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/476381/46e470ad-dff9-b494-701b-128abfa63be7.png" width=300>

loadViewやviewDidLoadでCGColorを再生成して設定すれば治りました。

## 余談②
#### RxSwiftで対応する
- RxSwift 5.0.1

RxSwiftでダークモードかどうかの判定を通知するObservableを作成しました。

作成したものの実際に使用することがなかったので、供養 :pray:

`UITraitEnvironment`を拡張して、`UserInterfaceStyle`がダークモードがどうかを通知するObservableを作成

```Swift
import RxSwift

extension Reactive where Base: UITraitEnvironment {
    var isDarkMode: Observable<Bool> {
        return sentMessage(#selector(Base.traitCollectionDidChange))
            .compactMap { $0.compactMap { $0 as? UITraitCollection }.first }
            .map { $0.userInterfaceStyle == .dark }
    }
}
```

#### 使用例

```Swift
import RxSwift

class viewController: UIViewController {
    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        rx.isDarkMode
            .subscribe(onNext: {
                // 処理
            })
            .disposeBag(by: disposeBag)
}
```