[iOS] UIColor.tintColorの伝播の裏側
iOSアプリには、そのアプリのアクセントとなるtintColor
が存在します。
デフォルトでは青色が設定されており、ボタンの塗りつぶしのデフォルトカラーとして使われたりします。
SwiftUIではColor.accentColor
が同様の役割を担っています。
tintColorを変更する
アプリのtintColorを変更するには、view.tintColor
を設定します。
すると、そのビュー階層より下のtintColor
は全てその色になります。
tintColorの変更を検知する
tintColor
が変更されると、その下のUIViewの全てのtintColorDidChange()
が呼ばれます。
drawなどで描画している場合は、ここで色の再描画を予約することで、カスタムビューへtintColorを反映させることができます。
UIViewの外でtintColorを取得できない
さて、ここまでの解説の通りtintColorは完全にUIViewの階層構造に依存しており単独で取り出すことができません。
では、UIViewはどのようにしてtintColorを取得しているのでしょうか?
UIColor.tintColorを解決する
UIColor.tintColorは、常にアプリで設定されたデフォルトのtintColorを返します。
そのため、たとえtintColorを変更したビュー階層の中で呼んでもview.tintColorの値は返ってきません。
一見すると、UIColor.tintColorはデフォルト値を返す単純なUIColorに見えますが実は特殊なUIColorになっています。
UIColor.tintColorのタイプを確認すると、UITintColor
という専用のクラスとして定義されていることが分かります。
type(of: UIColor.tintColor) // UITintColor
UITintColor
のヘッダを参照すると、UIDynamicColor
を継承しており、カラー解決が出来ることが分かります。
実際にtintColor
を変更したUIViewのtraitCollectionを渡してみましょう。
view.tintColor = .red
UIColor.tintColor.resolvedColor(with: view.traitCollection)
// -> UIExtendedSRGBColorSpace 1 0 0 1
すると、設定したtintColor
を取り出すことができました。
なぜtraitCollectionがUIColor.tintColorを解決できるのか
さて、最後に残った謎は、なぜtraitCollectionがUIColor.tintColorを解決できるのかということです。
例えばUIColor
では階層構造に応じて色を変える場合はtraitCollection
のuserInterfaceLevel
を参照しています。
しかし、UITraitCollection
のドキュメントにtintColor
に関するものは無さそうです。
公開されていないAPIを調べてみます。
view.traitCollection.perform(Selector("_methodDescription"))
すると、次のメソッドを見つけることができました。
- (void) _setTintColor:(id)arg1; (0x184fd2fd4)
- (id) _tintColor; (0x184fd2fbc)
このことから、内部的にUITraitCollectionはtintColorの実体を保持しており、おそらくview.tintColorは次のような実装になっていることが予想できます。
var tintColor: UIColor {
UIColor.tintColor.resolvedColor(with: traitCollection)
}
このような伝播の仕組みによってtintColorが実装されていることが分かりました。
また、iOS17からはCustom Traitが作れるようになりました。この実装を参考に伝播ロジックを考えてみるのも良さそうです。
Discussion