How to create a builder of `some View` types?

I'd like to create a view builder but not using the ViewBuilder DSL. Instead I'd like to create a more verbose builder with a syntax similar to:

struct HeaderView: View {
    let image: UIImage
    let title: String
    let subtitle: String

    var body: some View {
        var builder = VStackBuilder()
        builder.add(Image(uiImage: image))
        builder.add(Text(title))
        builder.add(Text(subtitle))
        return builder.build()
    }
}

This code is from: The Swift 5.1 features that power SwiftUI’s API | Swift by Sundell but not implemented, just used as an illustration of what is happening under the hood for the ViewBuilder DSL.

I figure the builder itself is appending to a list of views and when .build() is called it will then detect the length of the list and create a fixed sized TupleView and pass that to VStack. In this case I could use Array<AnyView> but I've been reading that for performance reasons AnyView cannot be optimized by the compiler, so I was looking at Array<some View> but this is not accepted by the compiler. So how does the ViewBuilder add new views generically and how can I replicate that?

That's not how it works behind the scenes. SwiftUI uses TupleViews with up to 10 elements. What ViewBuilder does is essentially calling ViewBuilder.buildBlock, which has variants for up to 10 parameters. It's not a builder.add function, it's

// here we are using ViewBuilder.buildBlock<T0, T1, T2>(_:_:_:) -> TupleView<(T0, T1, T2)>
let tupleView = ViewBuilder.buildBlock(
  Image(uiImage: image),
  Text(title),
  Text(subtitle)
)
return tupleView

So, depending on the number of inputs, the correct overload of ViewBuilder.buildBlock gets called and you get the right TupleView in return.

@xAlien95 I get that ViewBuilder.buildBlock is being called. What I am asking about is how SwiftUI is determining which arity of that function to call based upon the number of views passed into the DSL. For example, given this view:

VStack {
  Text("foo")
  Spacer()
}

How does SwiftUI determine to call ViewBuilder.buildBlock(v1, v2) and with this view:

HStack {
  Image(image.path)
}

how does SwiftUI determine to call ViewBuilder.buildBlock(v1) ?

Programmatically I would assume that the number of views in the DSL block are counted and the correct function arity is called but how to duplicate that is what I'm after.

It's a feature implemented in the compiler itself, SwiftUI has no access to the number of statements in a block. It, with the ViewBuilder @resultBuilder, can only provide various buildBlock static methods, then the compiler will do the rest and pick the right one.

You can read more about the @resultBuilder transformations in the Language Reference Guide and in the Swift Evolution proposal [SE-0289] Result Builders.

You can simplistically use multiple overloads:

struct MyList<Data: View> {
  init<T0>(_ element: T0) where Data == T0 { ... }
  init<T0, T1>(_ e0: T0, _ e1: T1) where Data == TupleView<(T0, T1)> { ... }
  init<T0, T1, T2>(_ e0: T0, _ e1: T1, _ e2: T2) where Data == TupleView<(T0, T1, T2)> { ... }
  // and so on...
}

var body: some View {
  MyList(
    Image(uiImage: image),
    Text(title),
    Text(subtitle)
  )
}

Build a view and then use type(of:) function to get the type. This should help understanding these bits. You can also call dump Or Mirror it if you want to find out the stored properties.

Terms of Service

Privacy Policy

Cookie Policy