View Builders

In the last post we looked at view modifiers as a way of building custom UI components that use other views as a form of content. In this one we’re going to look at a different approach using ViewBuilder.

We’re actually going to build the exact same card structure we built before:

character title in a card with shadow

When we used a ViewModifier this became a .card method we applied to the HStack like .frame or .padding, this time we’re going to make a block construct like HStack itself.

Our goal is that the code to make character card will look like:

struct ContentView : View {
    var body: some View {
        Card {
            HStack {
                Image("brawler")
                Text("Sir Bunnington")
                    .font(.title)
            }
        }
    }
}

To achieve this we’ll need Card to be a View again rather than a modifier, and we’ll use generics to handle all of the possible types of view that the content might be:

struct Card<Content> : View
    where Content : View
{
    var content: Content

    var body: some View {
        content
            .padding()
            .background(Color.white)
            .cornerRadius(8)
            .shadow(radius: 4)
    }
}

This approach in general is useful for views where there is a single view that needs to be passed in to the constructor. Good examples of this being used in SwiftUI include NavigationLink for the destination parameter.

But when we need a more complex child layout we don’t always want to have to abstract it into a custom View structure, and instead want to be able to use a block in at that point.

This is where view builders come in. @ViewBuilder is an attribute that we can declare on the parameters of methods we define, and most usefully, that includes the constructor.

So we can extend our above example to set the value of content from a view builder:

struct Card<Content> : View
    where Content : View
{
    var content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        content
            .padding()
            .background(Color.white)
            .cornerRadius(8)
            .shadow(radius: 4)
    }
}

The builder is a method that takes no arguments and returns a view, which we allow type inference to define as the Content our type is generic over. We set the property of that type by calling the method, which invokes the block passed.

And now we can use the Card view exactly as we intended in our goal code.

Patterns

The decision about whether to use a view, view modifier or a view builder is ultimately going to come down to what makes the most sense for your code.

Some like Button make sense as view builders.

But there are good examples in SwiftUI of view modifiers that instinct might suggest be view builders. .frame is a modifier on a view, there is no Frame builder, even though it’s placing the view inside it.

Only once you start combining multiple frames does it makes sense why it’s a modifier, since it’s easier to combine modifiers than it is to combine builders.

A good rule of thumb for me has to be to use a modifier first, and only use a builder when the code patterns really pulled strongly for that syntax.


Imagery used in previews by Kaiseto, original images and derived here licensed under Creative Commons 3.0 BY-NC-SA.