環境
- Xcode 11.5
- Swift 5.2.4
- iOS 13.0~
前口上
表題ではUIButtonとしていますが、UIColorで設定されているtextColorとの比較ができるのでUIButtonを用いているだけで、UITraitEnvironment
を継承しているUIView
やUIViewController
で対応可能です。
あと動的という表現が適切かどうかはわかりません。
問題点
UIButtonの枠線に色を設定するために、 layer.borderColor
にCGColorを設定します。
※比較のため、textColorとborderColorには同一のUIColorを用います。
button.titleLabel?.textColor = UIColor.label // UIColor
button.layer.borderColor = UIColor.label.cgColor // CGColor
しかし、上記の設定だけではUIButtonが生成された後、
UserInterfaceStyle
(以降style
と表記)を切り替えた場合に枠線のみ色が追従しません。
Light → Dark(ダークモードで生成した場合も同様)
Light | Dark |
---|---|
※背景ViewのbackgroundColorにはSystemBackgroundColorを適用しています |
どうやらCGColorで設定されている場合は、style
の変化には自動で追従できないようです。
対応
UIView
やUIViewController
が継承している UITraitEnvironment
のtraitCollectionDidChange(_:)
をoverrideして対応します。
style
の切り替えを検知して、都度CGColorを生成して設定し直す処理を加えます。
traitCollectionDidChange(_:)
に関しては公式ドキュメントを参照ください。
実装
style
が切り替わった度にtraitCollectionDidChange(_:)
が実行されます。
メソッド内部で、その時の端末のstyle
に合ったCGColorを生成して枠線に設定してあげます。
UIViewControllerで処理を行う場合
class HogeViewController: UIViewController {
/*
UI等の設定
*/
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
button.layer.borderColor = UIColor.label.cgColor
}
}
カスタムクラスを作成して処理を行う場合
以下のカスタムクラスを用いて枠線の色を追従させたいボタンを生成します。
class CustomButton: UIButton {
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
layer.borderColor = UIColor.label.cgColor
}
}
Light → Dark(ダークモードで生成した場合も同様)
Light | Dark |
---|---|
追加実装
上記の対応だけでも実現はできていますが、traitCollectionDidChange(_:)
のメソッドはstyle
以外の変化に対しても処理が走ってしまいます。
※検知対象であるUITraitCollection
に関しても公式ドキュメントを参照ください。
そこで、無駄を省くために以下の制御を追加します。
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を生成すると、枠線の色はライトモード用の色として生成されてしまいました。
loadViewやviewDidLoadでCGColorを再生成して設定すれば治りました。
余談②
RxSwiftで対応する
- RxSwift 5.0.1
RxSwiftでダークモードかどうかの判定を通知するObservableを作成しました。
作成したものの実際に使用することがなかったので、供養
UITraitEnvironment
を拡張して、UserInterfaceStyle
がダークモードがどうかを通知するObservableを作成
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 }
}
}
使用例
import RxSwift
class viewController: UIViewController {
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
rx.isDarkMode
.subscribe(onNext: {
// 処理
})
.disposeBag(by: disposeBag)
}