Result builders in Swift explained with code examples

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

A custom result builder can't handle a collections of components at first.
A custom result builder can’t handle a collections of components at first.

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:

A list of available methods inside a custom result builder definition.
A list of available methods inside a custom result builder definition.

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 write some more custom code.

We can solve this by defining a new protocol that gets implemented by both a single NSLayoutConstraint and a collection of constraints:

 protocol LayoutGroup {
     var constraints: [NSLayoutConstraint] { get }
 }
 extension NSLayoutConstraint: LayoutGroup {
     var constraints: [NSLayoutConstraint] { [self] }
 }
 extension Array: LayoutGroup where Element == NSLayoutConstraint {
     var constraints: [NSLayoutConstraint] { self }
 } 

This protocol allows us to convert both a single and a collection of constraints into an array of constraints. In other words, we can bring both types together into a common type [NSLayoutConstraint].

We can now rewrite our result builder implementation and allow it to receive our LayoutGroup protocol:

 @resultBuilder
 struct AutolayoutBuilder {
     
     static func buildBlock(_ components: LayoutGroup...) -> [NSLayoutConstraint] {
         return components.flatMap { $0.constraints }
     }
 } 

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: LayoutGroup...) -> [NSLayoutConstraint] {
         return components.flatMap { $0.constraints }
     }
     
     static func buildOptional(_ component: [LayoutGroup]?) -> [NSLayoutConstraint] {
         return component?.flatMap { $0.constraints } ?? []
     }
 } 

It tries to map the result into a collection of constraints or returns 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 block handlers we need to add to our function builder:


 @resultBuilder
 struct AutolayoutBuilder {     
     static func buildBlock(_ components: LayoutGroup...) -> [NSLayoutConstraint] {
         return components.flatMap { $0.constraints }
     }
     
     static func buildOptional(_ component: [LayoutGroup]?) -> [NSLayoutConstraint] {
         return component?.flatMap { $0.constraints } ?? []
     }
     
     static func buildEither(first component: [LayoutGroup]) -> [NSLayoutConstraint] {
         return component.flatMap { $0.constraints }
     }
 
     static func buildEither(second component: [LayoutGroup]) -> [NSLayoutConstraint] {
         return component.flatMap { $0.constraints }
     }
 } 

In both buildEither handlers we make use of the same LayoutGroup protocol method to get the constraints and return them flattened.

These were the last two build handlers required to make our example code work, awesome!

However, we’re not done. We can make this code just a little bit nicer by using result builders inside functions.

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!