🎨

[iOS] UIColor.tintColorの伝播の裏側

2024/04/29に公開

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を継承しており、カラー解決が出来ることが分かります。
https://developer.limneos.net/?ios=17.1&framework=UIKitCore.framework&header=UITintColor.h

実際に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では階層構造に応じて色を変える場合はtraitCollectionuserInterfaceLevelを参照しています。
しかし、UITraitCollectionのドキュメントにtintColorに関するものは無さそうです。
https://developer.apple.com/documentation/uikit/uitraitcollection

公開されていない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