[SwiftUI] Modifierをもっと便利に使う
最初に
SwiftUIでは、Viewに対してModifierを付与することによって、そのレイアウトを調整したり挙動を変更してアプリを構築していきます。
そのModifierには、padding
やframe
、foregroundStyle
などが標準で提供されていますが、ViewModifier
プロトコルを使って自作することも可能です。
本記事では、ちょっと踏み込んだModifierの作り方をViewModifier
を使ったり使わなかったりして解説していきます!
カスタムModifierを作る
今回は次の画像Viewを例にカスタムModifierの作り方を見ていきます
struct ContentView: View {
var body: some View {
Image(systemName: "hand.thumbsup.fill")
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.foregroundStyle(.blue)
}
}
上記の画像Viewには多くのModifierが設定されているようです。
もしアプリ内のあらゆる画像に対して一貫したスタイル適用をしなければいけない場合、Modifierをつけ忘れてしまったり、単純にコード量が増えたりでメンテナンス性が下がってしまうかもしれません。
struct ContentView: View {
var body: some View {
HStack {
Image(systemName: "hand.thumbsup.fill")
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.foregroundStyle(.blue)
Image(systemName: "pencil.circle.fill")
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.foregroundStyle(.blue)
Image(systemName: "person.crop.circle.fill")
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.foregroundStyle(.blue)
}
}
}
一貫したスタイルを適用する際には、それをカスタムModifierとしてまとめることが一般的に有効です。
それで実際にメンテナンス性の向上ができるか試してみましょう!
基本的なViewModifierを使ったカスタムModifierの作り方
ViewModifierを使ったカスタムModifierは次のようにして作ることができます
struct CustomImageThemeModifier: ViewModifier {
func body(content: Content) -> some View {
content
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.foregroundStyle(.blue)
}
}
extension View {
func customImageTheme() -> some View {
self
.modifier(CustomImageThemeModifier())
}
}
struct ContentView: View {
var body: some View {
Image(systemName: "hand.thumbsup.fill")
.renderingMode(.template)
.resizable()
.customImageTheme()
}
}
これでaspectRatio
、frame
、foregroundStyle
はcustomImageModifier()
にまとめることができましたが、renderingMode
・resizable
の2つはImageから生えているModifierであるため、Viewのextensionから生えているcustomImageModifier()
では扱うことができませんでした。
数行分だけModifierの削減はできましたが、もう少しやりようがありそうです。
固有ViewのModifierのまとめ方
固有のViewから生えているModifierをまとめる方法はシンプルです。
先ほどViewのextensionでcustomImageTheme()
を生やしたように、その固有Viewのextensionから関数を生やしてあげましょう。
extension Image {
func customImageTheme() -> some View {
self
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.foregroundStyle(.blue)
}
}
struct ContentView: View {
var body: some View {
Image(systemName: "hand.thumbsup.fill")
.customImageTheme()
}
}
これでImageに適用するModifierはcustomImageTheme()
の一つだけになり、可読性の問題は解決されていそうです!
しかしこのままではまだアプリ内全てのImageに対してcustomImageTheme()
を付与する必要があり、「Modifierをつけ忘れてしまう」問題に関してはまだ解決できていません。
SwiftUIでは、このような共通の設定をアプリ全体に対して適用するための仕組みが確かありましたよね?
そう、Environmentです
Environmentを使ってアプリ全体にThemeを流し込む
一つの操作でアプリ全体の画像に一貫したThemeを設定できるように、次のようにRoot部分にModifierを設定することで完結する形を目指して作成していきます!
struct ContentView: View {
var body: some View {
HStack {
Image(systemName: "hand.thumbsup.fill") // customImageThemeが適用される
Image(systemName: "pencil.circle.fill") // customImageThemeが適用される
Image(systemName: "person.crop.circle.fill") // customImageThemeが適用される
}
.customImageTheme()
}
}
まずはEnvironmentValuesと、その値を受け取って使うViewを用意しましょう。
EnvironmentValuesではsome View
を扱うことができないことに注意が必要です。
そのためAnyViewで型消しして渡してあげるようにします
extension EnvironmentValues {
@Entry var customThemeImage: (_ image: Image) -> AnyView = { AnyView($0) }
}
View側は、生のImageがThemeを受け取れるのが理想的ですが、Environmentの値をImageに直接渡せないため、カスタムViewを作ってこれを経由して受け取れるようにします。
今回はThemedImageViewと名付けてみました
struct ThemedImageView: View {
@Environment(\.customThemeImage) private var customThemeImage // : (Image) -> AnyView
let systemName: String
var body: some View {
customThemeImage(Image(systemName: systemName))
}
}
受け取るViewも作成できたので、Root部分から流しこむためのModifierを作りましょう!
Root部分はViewなので、今までと同じようにViewのextensionから生やします。
extension View {
func applyCustomImageTheme() -> some View {
self
.environment(\.customizingImage) { image in
let imageCustomized = image
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.foregroundStyle(.blue)
return AnyView(imageCustomized)
}
}
}
これにて、Envrionmentを活用して、一つの操作でアプリ全体の画像に一貫したThemeを設定できるようになりました!
全体像は↓です
struct ContentView: View {
var body: some View {
HStack {
ThemedImageView(systemName: "hand.thumbsup.fill")
ThemedImageView(systemName: "pencil.circle.fill")
ThemedImageView(systemName: "person.crop.circle.fill")
}
.applyCustomImageTheme()
}
}
extension EnvironmentValues {
@Entry var customThemeImage: (_ image: Image) -> AnyView = { AnyView($0) }
}
struct ThemedImageView: View {
@Environment(\.customThemeImage) private var customThemeImage // : (Image) -> AnyView
let systemName: String
var body: some View {
customThemeImage(Image(systemName: systemName))
}
}
extension View {
func applyCustomImageTheme() -> some View {
self
.environment(\.customThemeImage) { image in
let imageCustomized = image
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.foregroundStyle(.blue)
return AnyView(imageCustomized)
}
}
}
終わりに
基本的なカスタムModifierの作り方と、Environmentを使ったアプリ全体への流し込み方を解説しました。
実は、今回扱ったImageにおけるresizableのようなView固有のModifierでなければ、Environmentを使うにしてもEnvironmentalModifier
を使うなどして、もう少し簡潔に書く事もできたかと思います。まあ今回はエッジケースの紹介ということで。。。
本記事が誰かの課題を解決したり、実装のアイデアになることがあれば幸いです。
その時にはいいねください!
Discussion