最近ViewModifier Protocolというものを知ったので、簡単にメモ。

結論、このように書ける。

/// Usage:
///   Text("Text")
///       .modifier(BorderedFrame(color: Color.green))
struct BorderedFrame: ViewModifier {
    let color: Color

    func body(content: Content) -> some View {
        content
            .padding(5)
            .border(color, width: 1)
    }
}

以下詳細

SwiftUIのViewに対し、独自のmodifierを定義したい場合、今まで次のようにしていた。

/// Usage:
///   Text("Text")
///       .borderedFrame(Color.red)
extension View {
    func borderedFrame(_ color: Color) -> some View {
        BorderedFrame(color: color, content: self)
    }
}

/// View for modification
struct BorderedFrame<Content: View>: View {
    let color: Color
    let content: Content

    var body: some View {
        content
            .padding(5)
            .border(color, width: 1)
    }
}

正直この簡単な例では、わざわざmodification用のViewを新たに作らなくても直接extension内でselfに対しmodifierを生やしていけばよい。しかしEnvironmentValueを扱いたいなど要件が複雑になってくるとそうはいかず、このようなViewを定義してあげる必要がある。

このmodification用のViewを定義するためのずばりそのものなProtocolがSwiftUIにはあり、それがViewModifier Protocolである。このProtocolを使うと、記事冒頭のような形で書くことができる。

/// Usage:
///   Text("Text")
///       .modifier(BorderedFrame(color: Color.green))
struct BorderedFrame: ViewModifier {
    let color: Color

    func body(content: Content) -> some View {
        content
            .padding(5)
            .border(color, width: 1)
    }
}

個人的にはmodifierを明示的に書くのは微妙なので、次のように改めてViewのextensionとして生やすやり方を多用している。

/// Usage:
///   Text("Text)
///       .borderedFrame(Color.green)
extension View {
    func borderedFrame(_ color: Color) -> some View {
        modifier(BorderedFrame(color: color))
    }
}

このViewModifier Protocolにはconcatというメソッドも生えており、他のViewModifierを続けて使うこともできる。

/// Usage:
Text("Text")
    .modifier(BorderedFrame(color: Color.blue).concat(ShadowView(color: Color.green)))

/// View Modofiers
struct BorderedFrame: ViewModifier {
    let color: Color

    func body(content: Content) -> some View {
        content
            .padding(5)
            .border(color, width: 1)
    }
}

struct ShadowView: ViewModifier {
    let color: Color

    func body(content: Content) -> some View {
        content
            .padding(10)
            .shadow(color: color, radius: 10)
    }
}

適用順は直感通り、concatが呼ばれたViewが先、引数で渡されたViewが後の順で適用される。

Text("Text1")
      .modifier(BorderedFrame(color: Color.blue).concat(ShadowView(color: Color.green)))

Text("Text2")
    .modifier(ShadowView(color: Color.green).concat(BorderedFrame(color: Color.blue)))

ViewModifiers Result

とはいえ、可読性が微妙なので特段理由がなければ普通にそれぞれView Extensionとして定義して使う方が個人的には良いと思う。 modifiercontentメソッドが返すのはViewではなく、ModifiedContentというstructなので、実際に描画されるViewの生成を省略・遅延できるというメリットはありそう。きちんと検証すればパフォーマンスに差異がありそうだが、それはまた今度。