SwiftUI: Customizing differentView layouts with ResultBuilders
In SwiftUI there are multiple times when you declare a type of View
in your interface that contains some subviews that are visually arranged in some way. VStack
, HStack
and ZStack
are the most classical examples since they take some View
parameters and place them linearly across some axis(x, y or z), but there are other cases such as a List
, which works like a UITableView
,reusing its cells from the memory, and transforms each of its subviews into vertically positioned cells with some divider or a specific layout depending on the listStyle
modifier parameter or even a SwiftUI ScrollView
, which takes its subviews, place them linearly and make them scrollable.
You may be wondering, "Hey, what is the magic? It receives totally separated View
parameters and builds a new layout with them. How is that possible?". In order to understand this complex topic, we need to jump even deeper in the SwiftUI architecture and see how it parses the different parameters a component receives in order to transform it into a new one. Before you ask, no, the closure bellow is not a View itself:
Result Builders
Ok, let's talk about resultBuilders
. What are they? Basically it is a property wrapper that receives a closure syntax with some listed parameters and them return a single new output as a result to some computation the resultBuilder
class does. The input closure that lists the parameters is known as block. What this property wrapper really does is calling a function known as buildBlock
that parses the parameters of the block and returns a new result of any type, but in our case, we will be returning some View
:
Let's explain what's happening with some code. We will create a very simple View representing a cell that may receive a block of parameters that may consist of four scenarios:
- Two vertically aligned labels: a header and a subheader
- Two vertically aligned labels and a leading view: a header, a subheader and a leadingView
- Two vertically aligned labels and a trailing view: a header, a subheader and a trailingView
- Two vertically aligned labels plus a leading view and a trailing view: a header, a subheader, a leadingView and a trailingView
For that, we need four implementations of a buildBlock
including these types of inputs.
Creating our CellBuilder
Let's create a new enum that will be our resultBuilder
and implement our block parsing functions:
In order for it to work and parse the blocks it receives, we need to implement an overloading method called buildBlock
:
Basically what is happening is that each listed parameter of the CellBuilder
wrapper closure is being passed to the buildBlock
function, which returns a new View type. Here is the usage of it:
We are tagging the closure that returns a new View with the CellBuilder
property wrapper, and when we call the closure, it transforms the parameters into the new View and inserts it in the CellView
body.
As a result, the resultBuilder
will parse the parameters it receives into a new View and will insert it in the body. The buildBlock
may contain multiple types of parameters but we need to handle and implement each , let's check other cases:
Adding trailing and leading views to the cell
Let's suppose we now want to receive in our block besides the two strings a leading view, or a trailing view or maybe both. We need to implement three other variations:
And we can now call our View this way:
Also, if you have some variations for your resultBuilder
block that have the same inputs as other ones just with one less parameters, as in any other Swift function, you can make that parameter optional by providing a default value. Imagine you don't need the subheader in every case, so you can provide it as an optional and pass nothing as this parameter and have an if let
verification:
This is the same as if you had a custom block that only received the header
string and just placed it in the VStack
.
Dealing with ambiguities
Now imagine you have another variation for the block that instead of the leading and trailing views, you have some leading and trailing Texts
:
Text
conforms to the View
protocol, so which block would be called if we rendered this?
The answer is: Despite Text
being a View
type, the concrete type referring to Text
has more priority than a generic one that conforms to View
, so we are calling the block that receives Text
.
Conclusion
Apple provided a great shortcut for customizing different View layouts just by passing some parameters within a closure syntax. With that you can manage complex and different contexts just by defining the types of parameters you are expecting to your component and them mapping the parameter types into the respective block builders to result in different layouts. This makes SwiftUI an even more powerful tool and improves the reusability of your code. I hope this helps you simplify your Views and that you enjoyed ;).