Creating Optional @ViewBuilder Parameters in SwiftUI Views
SwiftUI allows the creation of very powerful custom views that take @ViewBuilder
closures, allowing you to nest views inside other views.
For example, let’s design a Card
view that allows you to pass content to be shown on a styled view.
Here’s the basic card:
To allow someone using our view to pass their own content, we can give the view a generic parameter conforming to View
, and use the @ViewBuilder
modifier to allow them to pass in a @ViewBuilder
closure.
@ViewBuilder
is a function attribute that allows you to write a closure in DSL-like syntax that returns a View.
Here’s our updated code:
Why do we use a closure in #3, rather than just a parameter of type Content
? It allows us to use the convenient @ViewBuilder
syntax when creating a Card
. Thus, we can do:
Card {
HStack {
Text("Hello")
Image(systemName: "face.smiling")
}
}
Instead of:
Card(content:
HStack {
Text("Hello")
Image(systemName: "face.smiling")
}
)
Our code above looks very similar, except now we can change what’s in the card just by changing the closure in previews
.
So we’ve seen how easy it is to let your view take a custom @ViewBuilder
closure.
But what if we want it to be optional?
What if we want the option to make a blank card, without making a brand new struct? First, we might try making the closure optional:
Unfortunately, you can’t apply @ViewBuilder
to an optional type. What if we try to take a closure of type () -> Content?
? This works alright:
However, it has a few downsides. One, we have to use the awkward syntax seen in line 24, which looks very weird. Second (and this might be a bug), it seems to break the preview in my instance of Xcode. Third, if we want to provide a default value if content
is nil, we’ll have to write some redundant if statement in our body
.
A better way
Ideally, we want to be able to write a constructor like Card()
, that automatically fills in the Content
type with something appropriate.
There’s a much better way to do this, and it matches Apple’s declaration. We’ll use an extension with generic where clause (simpler than it sounds). Basically, we create an extension providing an alternative init
method, but only provide the extension for a certain generic constraint.
This allows us to create elegant initializers for our views, similar to how Button
can take either a @ViewBuilder
or just a String.
Making it even more elegant
However, we can go one step further. With Swift 5.3, proposal SE-0267 allows us to write functions with where
clauses for generic types.
That means we can simplify the implementation shown above to:
This works great, and lets us keep all our init
s together! Here, we only wrote one alternative constructor, but you could certainly write more, such as a constructor that takes a string and makes Content
a Text
view.
Conclusion
We’ve seen how to easily write custom View
structs that act as “containers”, taking @ViewBuilder
closures and displaying their result. We’ve also solved the problem of writing elegant constructors when a @ViewBuilder
should be optional. Hopefully, you find this useful in writing powerful and beautiful custom views!