概要
SwiftUIを使ってアプリケーションを作成していると、Viewのコードが長くなり、Modifierの処理を共通化したいケースがでてくると思います。
そんな時にViewModifierを独自で定義して共通化するのも選択肢の1つです。
今回は、このような場合に独自でViewModifierを定義する方法について記述します。
環境
- Swift: 5.2.2
- Xcode: 11.4.1
独自のViewModifierの作り方
独自のViewModifierを作るには、 ViewModifier
プロトコルに準拠した struct を作成します。
// NOTE: 文字色を赤にするカスタムViewModifier struct CustomTextModifier: ViewModifier { func body(content: Content) -> some View { // NOTE: contentに元のViewが渡ってくるため加工したい内容を処理する content.foregroundColor(Color.red) } }
ViewModifier
は、Viewプロトコルと同じように body
という some View
を返すメソッドを1つだけ持ちます。
Viewプロトコルのbodyと異なるのは、bodyの引数に content
が渡ってくる点です。
この content に対して、加工したい処理を実装し some View を返すことで独自のModifierとして動作します。
Modifierの適用
struct ContentView: View { var body: some View { VStack { Text("Hello world") // NOTE: 独自のModifierを適用するには modifierメソッドを使う .modifier(CustomTextModifier()) } } }
独自のViewModifierをViewに適用するには、modifier
とうメソッドを使います。
modifier メソッドに対して、作成した独自のViewModifierを渡すことで適用することができます。
modifier メソッドは、ModifiedContent
という構造体を返します。
そのため、独自のModifierのもう一つの適用方法として、 ModifiedContentを直接定義するという方法も可能です。
struct ContentView: View { var body: some View { VStack { ModifiedContent(content: Text("Hello world"), modifier: CustomTextModifier()) } } }
この2つの方法はおそらく、同等の処理なのではないかと思います。
(おまけ) Viewのextentionで独自のViewModifierを作る
ViewModifierは、Content(適用前のView)
を受け取って、何か加工してから some View(適用後のVIew)
として返すというものなので、Viewに対して拡張(extension)する方法でも同じようなことができます。
extension View { func redText() -> some View { foregroundColor(Color.red) } } struct ContentView: View { var body: some View { VStack { Text("Hello world") .redText() } } }
この方法だとViewに対してメソッドを使いしているため、標準のModifierと似たような書き方ができます。
僕の場合は、例えばpadding
というViewModifierを拡張して使っています。
例えば、「上パディング」だけ設定したい時に他の値を定義するのが面倒なので、次のようなメソッドを追加して記述が少なくできるようにしています。
extension View { // NOTE: 他の値を設定するのが面倒なので、デフォルトで0を入れている func padding(top: CGFloat = 0, leading: CGFloat = 0, bottom: CGFloat = 0, trailing: CGFloat = 0) -> some View { padding(.init(top: top, leading: leading, bottom: bottom, trailing: trailing)) } // NOTE: 特定の箇所だけ設定する場合は、専用のメソッドを使う func paddingRight(_ value: CGFloat) -> some View { padding(trailing: value) } func paddingLeft(_ value: CGFloat) -> some View { padding(leading: value) } func paddingTop(_ value: CGFloat) -> some View { padding(top: value) } func paddingBottom(_ value: CGFloat) -> some View { padding(bottom: value) } } struct ContentView: View { var body: some View { VStack { Text("1") .padding(bottom: 10) Text("2") .paddingTop(100) } } }
個人的には、extensionで実現できるのであれば、こちらのほうがシンプルに書けて良いかなと思っていますが、ViewMofierプロトコルを使う場合と比べて何か副作用があるのか気にはなっています。
(適切にスコープする必要があるなど、ライブラリ提供をする際などは考えることはありそうですが。。)