Creating Optional @ViewBuilder Parameters in SwiftUI Views

Michael Ginn
3 min readOct 27, 2020

--

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:

Our card view

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.

A beautiful empty card

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 inits 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!

--

--

Michael Ginn

Computational Linguistics Ph.D. at Colorado, iOS Development, Perennial  Intern