しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

SwiftUIのViewBuilderについて調べてみる

SwiftUIのVStackやHStackでは、下のようにTextを2行並べて書くだけでTextを縦並びにできます。
今回はこの仕組みについて調べてみました。

struct ContentView: View {
    var body: some View {
        VStack {
            Text("AAA")
            Text("BBB")
        }
    }
}

f:id:llcc:20200322142651p:plain

上記のコードですが、下のような記述と同等になります。
複数個並べられていたTextは、ViewBuilderのbuildBlockの引数として使われます。

struct ContentView: View {
    var body: some View {
        VStack {
            ViewBuilder.buildBlock(
                Text("AAA"),
                Text("BBB")
            )
        }
    }
}

ViewBuilderのbuildBlockは、以下のようなTupleViewを返すメソッドです。
渡されたViewを元にTupleViewを作って返します。

extension ViewBuilder {
    public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View
}

まとめると、省略記法を使わずにVStackを使う場合は以下のような記述になります。

// 元コード
// struct ContentView: View {
//     var body: some View {
//         VStack {
//             Text("AAA")
//             Text("BBB")
//         }
//     }
// }

// 省略せずに書かれたコード
struct ContentView: View {
    var body: some View {
        VStack(content: { () -> TupleView<(Text, Text)> in
            return ViewBuilder.buildBlock(
                Text("AAA"),
                Text("BBB")
            )
        })
    }
}

続けて、ViewBuilderを省略できた理由を見ていきます。

省略できたのは、ViewBuilderの宣言についている@_functionBuilderというAttributesが関係しています。

@_functionBuilder public struct ViewBuilder {
}

@_functionBuilderを使ってStructを作ると以下の@MyStructのようにクロージャーに付けるAttributesが作られます。

func myFunc(@MyStruct closure: () -> Int) -> Int {
   return closure()
}

@_functionBuilder struct MyStruct {
    static func buildBlock(_ v1: Int) -> Int {
        return v1
    }
    
    static func buildBlock(_ v1: Int, _ v2: Int) -> Int {
        return v1 + v2
    }
    
    static func buildBlock(_ v1: Int, _ v2: Int, _ v3: Int) -> Int {
        return v1 + v2 + v3
    }
}

@MyStructを付けたクロージャーは、以下のような省略記法を使えます。
ViewBuilderもこの機能を使う事でViewBuilder.buildBlockという呼び出しを省略していました。

myFunc {
    1
    2
}
// 上は以下の省略形
myFunc {
    MyStruct.buildBlock(1, 2)
}

func myFunc(@MyStruct closure: () -> Int) -> Int {
    return closure()
}

余談ですが@_functionBuilderはclassにも使うことができます。

@_functionBuilder class MyClass {
}

以下のように値の数がbuildBlockの引数の数を超えるとエラーになります。

myFunc {
    1
    2
    3
    4
}

ViewBuilderは引数が最大10個なので、下のようにViewを11個渡すとエラーになります。

struct ContentView: View {
    var body: some View {
        VStack {
            Text("1")
            Text("2")
            Text("3")
            Text("4")
            Text("5")
            Text("6")
            Text("7")
            Text("8")
            Text("9")
            Text("10")
            Text("11")
        }
    }
}

参考URL

SwiftUIの魔法を実現する仕組み (Custom Attributes, Function Builder) - Qiita