3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-06-06

環境

  • Xcode 11.5
  • Swift 5.2.4
  • iOS 13.0~

前口上

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

問題点

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の変化には自動で追従できないようです。

対応

UIViewUIViewControllerが継承している UITraitEnvironmenttraitCollectionDidChange(_:)を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を作成しました。

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

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)
}
3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?