It’s now or never

IT系の技術ブログです。気になったこと、勉強したことを備忘録的にまとめて行きます。

【SwiftUI】独自のViewModifierを定義する方法(ViewModifierプロトコル、View extension)

概要

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プロトコルを使う場合と比べて何か副作用があるのか気にはなっています。
(適切にスコープする必要があるなど、ライブラリ提供をする際などは考えることはありそうですが。。)