🎨

[SwiftUI] 独自定義の色(enum)を直接モディファイアに渡す

2024/05/26に公開

最近までの話(enum から Color への変換)

アプリ内で使う色を enum で定義しています。

enum MyColor {
  case primary
  case secondary
}

このままでは使えないので、各フレームワークに合わせてそれぞれの色(UIColorSwiftUI.Color)に変換します。

extension MyColor {
  func toColor() -> Color {
    ...
  }
}

色を設定したいコンポーネントで上記メソッドを呼び、フレームワークに合った型に変換して使っていました。

Image(systemName: "globe")
    .imageScale(.large)
    .foregroundStyle(MyColor.primary.toColor())

SwiftUI のモディファイアを知る

foregroundStylebackgroundStyle は、名前からして style を渡すことのできるモディファイアだと認識していましたが、 Color を渡せることが少し疑問でした。
foregroundStyle モディファイアの定義を見ると、 Viewextension として以下のように定義されていました。

@inlinable public func foregroundStyle<S>(_ style: S) -> some View where S : ShapeStyle

ColorShapeStyle に適合しているため、各モディファイアに渡すことができたようです。

独自定義の色に ShapeStyle を適合させる

ということであれば、独自定義の MyColorShapeStyle を実装すれば、変換せずとも各モディファイアに直接渡すことができそうです。
ShapeStyle の定義は以下のとおりです。

https://developer.apple.com/documentation/swiftui/shapestyle

ColorShapeStyle に適合しているので、 Resolved の型を Color にして、 resolve(in:) を実装すれば良さそうです。

extension MyColor: ShapeStyle {
    typealias Resolved = Color
    func resolve(in environment: EnvironmentValues) -> Color {
        self.toColor()
    }
}

ShapeStyle に適合させることで、各モディファイアに渡す際の変換が不要になりました。

Image(systemName: "globe")
    .imageScale(.large)
    .foregroundStyle(MyColor.primary)

標準で用意されているスタイルはさらに短い記述

SwiftUI のプロジェクトを作成したとき、 foreground には .tint という値が設定されていました。

Image(systemName: "globe")
    .imageScale(.large)
    .foregroundStyle(.tint)

. から始められるのは、なんだかかっこよさげです。
.tint の定義を見ていくと、 ShapeStyle の拡張として、静的プロパティが定義されていることがわかりました。
同様の記述を行えば、独自の色定義もさらに短い記述ができるはずです。

extension ShapeStyle where Self == MyColor {
    static var primaryColor: MyColor { .primary }
    static var secondaryColor: MyColor { .secondary }
}

.primary のまま定義することはできないので、 .primaryColor と名前を変えています。

Image(systemName: "globe")
    .imageScale(.large)
    .foregroundStyle(.primaryColor)

このように . から記述することができました。
ただ、せっかく enum で定義しているのに、それぞれ対応した静的プロパティを用意しないといけないのが面倒な気がしました。 enum の定義が多い場合は、ここまでやるメリットは薄いかもしれないですね。

おわりに

  1. メソッドによる型変換
  2. ShapeStyle への適合
  3. ShapeStyle の拡張静的プロパティ

と、徐々にモディファイアへ渡す際の記述が短くなるような実装を紹介しました。
今回は色のみを扱いましたが、まだまだいろいろできることがありそうです。
色だけなら 2 までで十分かな、と思いましたが、アプリ全体を通してスタイルが決まっているようなら、 3 までかっちり定義するのもアリかもしれません。

追記

iOS 17 以降でなければ使えなかった。

うまくできたと思ったのですが、実際に試してみるときちんと色が反映されない端末がありました。
よくよく resolve(in:) メソッドを見てみると iOS 17.0+ と書かれているじゃないですか…。

https://developer.apple.com/documentation/swiftui/shapestyle/resolve(in:)-mq46

さすがに現時点で iOS 17 以降のみをサポートしているアプリはごく少数かと思います。
まだしばらくは使えなさそうです。残念…。

Discussion