概要
SwiftUIのViewの書き方には今までのSwiftにはなかった特徴があります。
次のような、クロージャにViewを連続で渡すようなDSL記法です。
今までのSwiftを書いたことがある人は、「なぜこんな書き方ができるんだろう?」と疑問に思われた人もいるかもしれません。
struct ContentView: View { var body: some View { VStack { Text("1行目") Text("2行目") Text("3行目") } } }
これには、@ViewBuilder
という仕組みが使われているのですが、今回はこの @ViewBuilderとその元の拡張につかわれている @_functionBuilder
について調べてみました。
環境
- Swift: 5.2
- Xcode: 11.4
VStackのイニシャライザを確認
今回は、VStack
という標準ViewComponentを例にみてみます。
まずは、VStackのイニシャライザを確認してみましょう。
@frozen public struct VStack<Content> : View where Content : View { @inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content) public typealias Body = Never }
init() の引数の最後にクロージャが定義されています。
VStack { Text("1行目") Text("2行目") Text("3行目") }
これが、上記の{}
で囲まれた部分です。
VStackのイニシャライザのクロージャには、@ViewBuilder
という修飾子がついています。
この @ViewBuilder
がDSL記法を実現している仕組みの箇所です。
@ViewBuilder
次は、@ViewBuilder
の中身を見ていきます。
@_functionBuilder public struct ViewBuilder { public static func buildBlock() -> EmptyView public static func buildBlock<Content>(_ content: Content) -> Content where Content : View }
ViewBuilder structの定義は上記のようになっています。
buildBlock
というメソッドが2つ定義されており、1つはContentが引数として渡されます。
なんとなくこのContentにクロージャで返す、Contentが渡されてくるのではないかと想像できます。
このViewBuilderは @_functionBuilder
という拡張が定義されています。
この @_functionBuilder
という拡張がこの記法を実現している仕組みです。
@_functionBuilder
@_functionBuilder
拡張は、クロージャの引数に付与する修飾を定義するための拡張
で、この@_functionBuilderで作られた修飾をつけられたクロージャは出力されるまえに buildXXXX
で定義されたメソッドで処理されます。
struct VStack<Content> : View where Content : View { init(... @ViewBuilder content: () -> Content) }
VStack { Text("1行目") Text("2行目") Text("3行目") }
つまり、今回の場合、このVStackに渡される3つのTextは一度buildBlock
メソッドで処理され結果が出力されるということになります。
複数のViewが渡される仕組み
ViewBuilderでは、複数のViewを処理するために、その数分の引数を持つ buildBlock
が定義されています。
ViewBuilder
の定義を見ていくと、 buildBlock
メソッドは 2つだけではなく、extentionとして他にも定義されています。
extension ViewBuilder { public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View public static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> TupleView<(C0, C1, C2)> where C0 : View, C1 : View, C2 : View public static func buildBlock<C0, C1, C2, C3>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3) -> TupleView<(C0, C1, C2, C3)> where C0 : View, C1 : View, C2 : View, C3 : View ・・・ public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View }
このように引数が最大10個まで buildBlock
メソッドが拡張されています。
VStack { Text("1行目") Text("2行目") Text("3行目") }
例えば上記のような、3つのViewが渡されるクロージャでは、次のような3つの引数の buildBlock
を使って処理をします。
public static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> TupleView<(C0, C1, C2)> where C0 : View, C1 : View, C2 : View
つまり、クロージャの1構文(=1View)が各buildBlockメソッドの引数に割り当てられるということです。
このため、現在のバージョンでは、指定するクロージャでは、最大10個までしかViewを渡せない
という制限があります。
また buildBlockメソッドでは、複数のViewを処理する場合は、TupleView
というタプルをイニシャライザに渡せるViewをつかって1つのViewとして返しています。
試してみる
なんとなく、ViewBuilderについて理解が深まってきたので実際にコードで試してみます。
@_functionBuilder struct CustomViewBuilder { static func buildBlock<Content>(_ content: Content) -> Content where Content : View { content } static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View { // NOTE: TupleViewという複数のViewをタプルで受け取って1つのViewにするViewを使う TupleView((c0, c1)) } }
まずは、@_functionBuilder CustomViewBuilder
を作ります。
このViewBuilderでは引数に2つまでしかViewを受け取れません。
また、1つのViewを返すために、タプルで複数のViewを受け取れる TupleView
というViewを使っています。
struct MyVStack<Content>: View where Content: View { var content: () -> Content // NOTE: CustomViewBuilderをつけることでクロージャにViewを複数受け取れるようになる init(@CustomViewBuilder content: @escaping () -> Content) { self.content = content } var body: some View { VStack { self.content() } } }
このCustomViewBuilderを使って、contentを受け取れるようなViewを作っていきます。
struct MainView: View { var body: some View { VStack { MyVStack { Text("1行目") Text("2行目") } } } }
所感
初めは???となることも多かったViewBuilderですが仕組みがわかってしまえば、「なるほど〜」という感じでした。
bodyの直下では、普通にforやif構文が使えるのにViewの引数のクロージャではifしか使えないなどの理由がわかりスッキリしました。
個人的には、クロージャの構文をパースして引数に割り当てる処理がどのように実現されてるのかが気になりましたが、swiftのコードを読めば理解できるのでしょうか?(余裕があればいつか見てみたいです)