Result builders in Swift allow you to build up a result using ‘build blocks’ lined up after each other. They were introduced in Swift 5.4 and are available in Xcode 12.5 and up. Formerly known as function builders, you’ve probably already used them quite a bit by building a stack of views in SwiftUI.
I have to admit: at first, I thought that this was quite an advanced feature in Swift I wouldn’t ever use myself to write custom solutions to configuring my code. However, once I played around and wrote a little solution for building up view constraints in UIKit, I found out that it’s all about understanding the power of result builders.
What are result builders?
A result builder can be seen as an embedded domain-specific language (DSL) for collecting parts that get combined into a final result. A simple SwiftUI view declaration uses a @ViewBuilder
under the hood which is an implementation of a result builder:
struct ContentView: View {
var body: some View {
// This is inside a result builder
VStack {
Text("Hello World!") // VStack and Text are 'build blocks'
}
}
}
Each child view, in this case a VStack
containing a Text
, will be combined into a single View
. In other words, the View
building blocks are built into a View
result. This is important to understand as it explains how result builders work.
If we look into the declaration of the SwiftUI View
protocol we can see the body variable being defined using the @ViewBuilder
attribute:
@ViewBuilder var body: Self.Body { get }
This is exactly how you can use your custom result builder as an attribute of function, variable, or subscript.
Creating a custom result builder
To explain to you how you can define your own custom result builder I like to follow along on an example I’ve been using myself. When writing autolayout in code it’s common for me to write the following kind of logic:
var constraints: [NSLayoutConstraint] = [
// Single constraint
swiftLeeLogo.centerXAnchor.constraint(equalTo: view.centerXAnchor)
]
// Boolean check
if alignLogoTop {
constraints.append(swiftLeeLogo.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor))
} else {
constraints.append(swiftLeeLogo.centerYAnchor.constraint(equalTo: view.centerYAnchor))
}
// Unwrap an optional
if let fixedLogoSize = fixedLogoSize {
constraints.append(contentsOf: [
swiftLeeLogo.widthAnchor.constraint(equalToConstant: fixedLogoSize.width),
swiftLeeLogo.heightAnchor.constraint(equalToConstant: fixedLogoSize.height)
])
}
// Add a collection of constraints
constraints.append(contentsOf: label.constraintsForAnchoringTo(boundsOf: view)) // Returns an array
// Activate
NSLayoutConstraint.activate(constraints)
As you can see, we have quite a few conditional constraints. This can make it hard to read through constraints in complex views.
Result builders are a great solution to this and allow us to write the above sample code as follows:
@AutoLayoutBuilder var constraints: [NSLayoutConstraint] {
swiftLeeLogo.centerXAnchor.constraint(equalTo: view.centerXAnchor) // Single constraint
if alignLogoTop {
swiftLeeLogo.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
} else {
swiftLeeLogo.centerYAnchor.constraint(equalTo: view.centerYAnchor) // Single constraint
}
if let fixedLogoSize = fixedLogoSize {
swiftLeeLogo.widthAnchor.constraint(equalToConstant: fixedLogoSize.width)
swiftLeeLogo.heightAnchor.constraint(equalToConstant: fixedLogoSize.height)
}
label.constraintsForAnchoringTo(boundsOf: view) // Returns an array
}
Amazing, right?
So let’s see how we can build this custom implementation.
Defining the AutoLayout builder
We start by defining our custom AutoLayoutBuilder
struct and add the @resultBuilder
attribute to mark is as being a result builder:
@resultBuilder
struct AutoLayoutBuilder {
// .. Handle different cases, like unwrapping and collections
}
To build up a result out of all building blocks, we need to configure handlers for each case, like handling optionals and collections. But before we do, we start by handling the case of a single constraint.
This is done by the following method:
@resultBuilder
struct AutoLayoutBuilder {
static func buildBlock(_ components: NSLayoutConstraint...) -> [NSLayoutConstraint] {
return components
}
}
The method takes a variadic parameter of components, which means it can either be one or many constraints. We need to return a collection of constraints, which means, in this case, we can return the input components directly.
This now allows us to define a collection of constraints as follows:
@AutoLayoutBuilder var constraints: [NSLayoutConstraint] {
// Single constraint
swiftLeeLogo.centerXAnchor.constraint(equalTo: view.centerXAnchor)
}
Handling a collection of build blocks
Up next is handling a collection of build blocks as a single element. In our first code example, we used a convenience method constraintsForAnchoringTo(boundsOf:)
returning multiple constraints in a collection. If we would use that with our current implementation, the following error would occur:
The error description explains best what’s going on:
Cannot pass array of type ‘[NSLayoutConstraint]’ as variadic arguments of type ‘NSLayoutConstraint’
Variadic parameters in Swift do not allow us to pass in an array, even though it might seem logical to do so. Instead, we need to define a custom method for handling collections as component input. Looking at the available methods, you might think we need the following method:
Unfortunately, as the method description states, this only enables support for loops that combine the results into a single result. We don’t use an iterator but a convenient method to return a collection directly, so we need to change our code in a different way.
Instead, we’ll rewrite our building block method to take an array:
static func buildBlock(_ components: [NSLayoutConstraint]...) -> [NSLayoutConstraint] {
components.flatMap { $0 }
}
This change will break the initial implementation we’ve had since we can no longer pass in a single element. To add support for single elements, we need to add a conversion method, which can be achieved using the expression building methods:
@resultBuilder
struct AutoLayoutBuilder {
static func buildBlock(_ components: [NSLayoutConstraint]...) -> [NSLayoutConstraint] {
components.flatMap { $0 }
}
/// Add support for both single and collections of constraints.
static func buildExpression(_ expression: NSLayoutConstraint) -> [NSLayoutConstraint] {
[expression]
}
static func buildExpression(_ expression: [NSLayoutConstraint]) -> [NSLayoutConstraint] {
expression
}
}
These two methods allows us to convert a single layout constraint into a collection of constraints. In other words, we can bring both types together into a common type [NSLayoutConstraint]
that is accepted as a variadic array value.
Inside the buildBlock
method we use flatMap
to map into a single collection of constraints. If you don’t know what flatMap
does or why we don’t use compactMap
, you can read my article CompactMap vs flatMap: The differences explained.
Finally, we can update our implementation to make use of our new collection build block handler:
@AutoLayoutBuilder var constraints: [NSLayoutConstraint] {
// Single constraint
swiftLeeLogo.centerXAnchor.constraint(equalTo: view.centerXAnchor)
label.constraintsForAnchoringTo(boundsOf: view) // Returns an array
}
Handling unwrapping of optionals
Another case we need to handle is unwrapping optionals. This allows us to conditionally add constraints if a value exists.
We do this by adding the buildOptional(..)
method to our function builder:
@resultBuilder
struct AutoLayoutBuilder {
static func buildBlock(_ components: [NSLayoutConstraint]...) -> [NSLayoutConstraint] {
components.flatMap { $0 }
}
/// Add support for both single and collections of constraints.
static func buildExpression(_ expression: NSLayoutConstraint) -> [NSLayoutConstraint] {
[expression]
}
static func buildExpression(_ expression: [NSLayoutConstraint]) -> [NSLayoutConstraint] {
expression
}
/// Add support for optionals.
static func buildOptional(_ components: [NSLayoutConstraint]?) -> [NSLayoutConstraint] {
components ?? []
}
}
It returns the collection of constraints or an empty collection if the value does not exist.
This now allows us to unwrap an optional within our building blocks definition:
@AutoLayoutBuilder var constraints: [NSLayoutConstraint] {
// Single constraint
swiftLeeLogo.centerXAnchor.constraint(equalTo: view.centerXAnchor)
label.constraintsForAnchoringTo(boundsOf: view) // Returns an array
// Unwrapping an optional
if let fixedLogoSize = fixedLogoSize {
swiftLeeLogo.widthAnchor.constraint(equalToConstant: fixedLogoSize.width)
swiftLeeLogo.heightAnchor.constraint(equalToConstant: fixedLogoSize.height)
}
}
Handling conditional statements
Another common case to handle is conditional statements. Based on a boolean value you want to add one constraint or another. This build block handler basically works by being able to handle either the first or the second component in a conditional check:
@AutoLayoutBuilder var constraints: [NSLayoutConstraint] {
// Single constraint
swiftLeeLogo.centerXAnchor.constraint(equalTo: view.centerXAnchor)
label.constraintsForAnchoringTo(boundsOf: view) // Returns an array
// Unwrapping an optional
if let fixedLogoSize = fixedLogoSize {
swiftLeeLogo.widthAnchor.constraint(equalToConstant: fixedLogoSize.width)
swiftLeeLogo.heightAnchor.constraint(equalToConstant: fixedLogoSize.height)
}
// Conditional check
if alignLogoTop {
// Handle either the first component:
swiftLeeLogo.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
} else {
// Or the second component:
swiftLeeLogo.centerYAnchor.constraint(equalTo: view.centerYAnchor)
}
}
This reflects back into the build either handlers we need to add to our function builder:
@resultBuilder
struct AutoLayoutBuilder {
static func buildBlock(_ components: [NSLayoutConstraint]...) -> [NSLayoutConstraint] {
components.flatMap { $0 }
}
/// Add support for both single and collections of constraints.
static func buildExpression(_ expression: NSLayoutConstraint) -> [NSLayoutConstraint] {
[expression]
}
static func buildExpression(_ expression: [NSLayoutConstraint]) -> [NSLayoutConstraint] {
expression
}
/// Add support for optionals.
static func buildOptional(_ components: [NSLayoutConstraint]?) -> [NSLayoutConstraint] {
components ?? []
}
/// Add support for if statements.
static func buildEither(first components: [NSLayoutConstraint]) -> [NSLayoutConstraint] {
components
}
static func buildEither(second components: [NSLayoutConstraint]) -> [NSLayoutConstraint] {
components
}
}
In both buildEither
handlers we simply forward the received collection of constrained. These were the last two build handlers required to make our example code work, awesome!
Handling for..in loops
Although our example already works, there are a few more methods we can add to make our result builder more complete. For example, we can add support for loops:
/// Add support for loops.
static func buildArray(_ components: [[NSLayoutConstraint]]) -> [NSLayoutConstraint] {
components.flatMap { $0 }
}
Doing so allows us to use loops inside the result builder:
NSLayoutConstraint.activate {
for _ in (0..<Int.random(in: 0..<10)) {
swiftLeeLogo.centerXAnchor.constraint(equalTo: view.centerXAnchor) // Single constraint
}
}
Although the above example is not really useful, it does demonstrate using loops with result builders.
Supporting availability APIs
We’ve added support for if statements before, allowing us to write if statements using availability APIs as follows:
if #available(iOS 13, *) {
swiftLeeLogo.centerXAnchor.constraint(equalTo: view.centerXAnchor) // Single constraint
}
Since we’re not using any unavailable APis in this example the code compiles just fine. However, if you want to prepare for all availability statements, you can add support as follows:
/// Add support for #availability checks.
static func buildLimitedAvailability(_ components: [NSLayoutConstraint]) -> [NSLayoutConstraint] {
components
}
Using result builders as function parameters
A great way to make use of result builders is by defining them as a parameter of a function. This way, we can really benefit from our custom AutoLayoutBuilder
.
For example, we could make this extension on NSLayoutConstraint
to make it a little bit easier to activate constraints:
extension NSLayoutConstraint {
/// Activate the layouts defined in the result builder parameter `constraints`.
static func activate(@AutoLayoutBuilder constraints: () -> [NSLayoutConstraint]) {
activate(constraints())
}
}
Using it looks as follows:
NSLayoutConstraint.activate {
// Single constraint
swiftLeeLogo.centerXAnchor.constraint(equalTo: view.centerXAnchor)
label.constraintsForAnchoringTo(boundsOf: view) // Returns an array
// Unwrapping an optional
if let fixedLogoSize = fixedLogoSize {
swiftLeeLogo.widthAnchor.constraint(equalToConstant: fixedLogoSize.width)
swiftLeeLogo.heightAnchor.constraint(equalToConstant: fixedLogoSize.height)
}
// Conditional check
if alignLogoTop {
// Handle either the first component:
swiftLeeLogo.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
} else {
// Or the second component:
swiftLeeLogo.centerYAnchor.constraint(equalTo: view.centerYAnchor)
}
}
And now that we have this method in place, we can also create a convenient method on UIView
to add a subview directly with constraints:
protocol SubviewContaining { }
extension UIView: SubviewContaining { }
extension SubviewContaining where Self == UIView {
/// Add a child subview and directly activate the given constraints.
func addSubview<View: UIView>(_ view: View, @AutoLayoutBuilder constraints: (Self, View) -> [NSLayoutConstraint]) {
addSubview(view)
NSLayoutConstraint.activate(constraints(self, view))
}
}
Which we can use as follows:
let containerView = UIView()
containerView.addSubview(label) { containerView, label in
if label.numberOfLines == 1 {
// Conditional constraints
}
// Or just use an array:
label.constraintsForAnchoringTo(boundsOf: containerView)
}
As we use generics, we can make conditional checks based on the input type of the UIView
. In this case, we could add different constraints if our label will only have one line of text.
How to come up with custom result builder implementations?
I hear you thinking: how do I know that a result builder will be useful for a certain piece of code?
Whenever you see a piece of code that’s built out of several conditional elements and turned into a single common piece of the return type, you could think about writing result builders. However, only do so if you know you need to write it more often.
When you’re writing autolayout constraints in code, you’re doing that in a lot of places. Therefore, it’s worth writing a custom result builder for it. Constraints are also built out of multiple ‘building blocks’ once you see each collection of constraints (either single or not) as an individual building block.
Lastly, I’d like to reference this repository containing examples of function builders, now called result builders.
Conclusion
Result builders are a super powerful addition to Swift 5.4 and allow us to write custom domain-specific language that can really improve the way we write code. I hope that after reading this article it’s a bit easier for you to think of custom function builders that can simplify your code at the implementation level.
If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.
Thanks!