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:
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.