Result builders in Swift (1)
I am bringing back an older post, originally published three years ago, in July 2022. I have added two more: the second part of this one and an explanation of how arrays with different data types can be defined in Swift.
I have added an addendum at the end, generated by GPT o3, commenting on the changes introduced in Swift over the last three years that affect the topics discussed in the article.
Since Apple introduced SwiftUI at WWDC19, I have wanted to understand the Swift features on which this technology is built. I read a few posts that touched on the subject, and I came away with the idea that in Swift 5.1 they had introduced something called function builders, which was the feature that made it possible to build SwiftUI views declaratively, but I did not continue studying the subject any further.
One strange thing about function builders was that they were an undocumented Swift feature that had not gone through the usual language evolution process, in which proposals for new features are eventually approved or rejected after open discussion with the community.
It did not take long for a proposal and a pitch to appear on the community forums. The discussions went on, different alternatives were considered, the feature was renamed result builders, and in the end, almost two years later, it was accepted in October 2020 and published in the language in version 5.4, released in April 2021.
More than a year later, I have finally sat down to study result builders and try to understand how they work. After spending a few days reading documentation, creating some notes in Obsidian, and experimenting with Swift code, it is time to try to put everything in order and write a post on the topic.
Purpose of result builders
Let us begin by explaining the purpose of result builders, and then we will explain how they work.
An example with SwiftUI
If we look at a simple example of SwiftUI code, we will see that we can identify it as Swift code, but that there is something about it that does not quite fit. For example, the following code constructs a view in which an image and a piece of text are stacked vertically.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
}
}
The result is the following:

The code defines a struct named ContentView that conforms to the View protocol. This protocol requires a body property to be defined, which must also conform to View, thus recursively constructing a tree of views that SwiftUI is responsible for rendering.
The body property is a computed property of type some View that returns a VStack. We will leave the use of some for another post and focus on the construction of the VStack:
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
The braces after VStack define a trailing closure that is passed to the initializer. It is equivalent to:
VStack(content: {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
})
If we look closely at the code inside the closure, we will see that something is odd. There are two statements that construct an Image instance and a Text instance. They are precisely the image and the text that are stacked and shown in the resulting view. But nothing is done with those instances. How are they passed to VStack? Where is the closure’s return? What kind of magic is this?
The explanation is that SwiftUI defines a result builder that performs a compile-time transformation of the code above, code that is not valid Swift on its own, into code similar to the following:
VStack {
let v0 = Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
let v1 = Text("Hello, world!")
return ViewBuilder.buildBlock(v0, v1)
}
This code is valid Swift. The instances created by Image and Text are stored in two auxiliary variables, and a static function, ViewBuilder.buildBlock, is called. It receives those two views and combines them into a structure, a pair, that is also of type View and is returned.
Although we have not seen it in the example, it would also be possible to construct the constituent elements recursively using the same DSL. For example, one of the elements passed to the VStack could itself be another VStack formed by combining other basic elements.
Creating DSLs
Using the result builder above, we can transform the clean and clear code at the beginning, which does not work in Swift, into compilable code. The result builder adds everything needed, temporary variables, the call to the construction function, and so on, so that the resulting code is correct for the compiler. And it does so completely transparently. The developer does not see any of the second form of the code, only the first, the clean and clear version.
The code transformed by the result builder is what is known as a DSL, a Domain Specific Language. In this case, the DSL lets us construct SwiftUI views by describing and combining their constituent elements.
Result builders have not only been used to build SwiftUI. The community has created a large collection of DSLs for defining all kinds of things, such as HTML, CSS, graphs, REST functions, or tests. Even at the recent WWDC22, a DSL for building regular expressions in Swift was introduced, SwiftRegex.
In short, much like macros in programming languages such as LISP, or the define directives in C, result builders allow us to specify transformations that will be applied to the source code at compile time. Let us now see how that functionality has been incorporated into Swift.
First example
First of all, to define a result builder we need to specify a buildBlock function that constructs a result from a number of elements. In the case of the previous example, it needs to construct a composition of two views from the individual views, the Image and Text instances.
How can we define this function? The simplest way is to define a static function, one that can be called without needing to create an instance. This function must be named buildBlock, and it must take the individual components as parameters and return a new component resulting from their composition. We can define it in a structure, a class, or an enum annotated with the @resultBuilder attribute.
A very simple example that works with strings is the following:
@resultBuilder
struct StringConcatenator {
static func buildBlock(_ component1: String, _ component2: String) -> String {
return component1 + ", " + component2
}
}
The buildBlock function takes two strings and returns their concatenation, separated by a comma. We define it as a static function of the StringConcatenator structure. The @resultBuilder attribute indicates that this type is a result builder and that we will be able to specify a DSL with it.
How can we now indicate that we want to use this result builder? The Swift engineers came up with a brilliant idea. When the StringConcatenator type is defined as a result builder, the compiler creates the @StringConcatenator attribute, which we can use wherever we want to apply it.
For example, we can write the following code:
@StringConcatenator
func holaMundo() -> String {
"Hola"
"mundo"
}
print(holaMundo())
The holaMundo() function would not be valid Swift because it has no return with the string it is supposed to return. In addition, its two statements do nothing except define the strings "Hola" and "mundo". But if we run the code above, we will see that the compiler produces no error and that the code runs correctly and prints the typical message:
Hola, mundo
What is going on? By using the @StringConcatenator attribute on the holaMundo() function, we are declaring that it is a function whose body we are defining with a DSL that will be processed by the StringConcatenator result builder.
As in the previous SwiftUI example, each statement in the function body specifies a component that the compiler must process. In this case, they are strings. And at the end, buildBlock must be called to combine those components and return the resulting string. Specifically, the code produced by the transformation is the following:
func holaMundo() -> String {
let v0 = "Hola"
let v1 = "mundo"
return StringConcatenator.buildBlock(v0, v1)
}
This transformed code is what is actually executed in the program and what returns the string "Hola, mundo".
Variable number of arguments
In the previous example, the buildBlock function is defined only for two arguments. It would not work if we wanted to construct a string with more than two components. We can improve it by using Swift’s ability to define functions with a variable number of arguments:
@resultBuilder
struct StringConcatenator {
static func buildBlock(_ components: String...) -> String {
return components.joined(separator: ", ")
}
}
Now the buildBlock function receives a variable number of strings stored in the components array. And the higher-order function joined traverses the array of strings and joins them all with a comma and a space.
With this buildBlock, we can compose as many strings as we want in the DSL. For example, we can define a greeting from four strings:
@StringConcatenator
func saludo(nombre: String) -> String {
"Hola"
"me"
"llamo"
nombre
}
In this example, we have also added a nombre parameter to the function. This parameter lets us specify the name of the person greeting us.
The @StringConcatenator result builder transforms the code above into:
func saludo(nombre: String) -> String {
let v0 = "Hola"
let v1 = "me"
let v2 = "llamo"
let v3 = nombre
return StringConcatenator.buildBlock(v0, v1, v2, v3)
}
If we call the original function
print(saludo(nombre: "Frodo"))
it will print the following:
Hola, me, llamo, Frodo
DSL in computed variables
According to the official Swift documentation, we can use a result builder attribute in the following places:
- In a function declaration, and the result builder constructs the body of the function.
- In a variable declaration that includes a getter, and the result builder constructs the body of the getter.
- In a closure parameter of a function declaration, and the result builder constructs the body of the closure passed to the corresponding argument.
We saw the first case in the previous section. Let us look at an example of the second case.
For example, we can define the following structure:
struct Persona {
let nombre: String
@StringConcatenator
var saludo: String {
"Hola"
"me"
"llamo"
nombre
}
}
let frodo = Persona(nombre: "Frodo")
print(frodo.saludo)
Now the DSL is used to define the getter of the computed variable saludo. The result builder transforms that getter in the same way as in the previous examples, creating a getter that returns a string from the strings that appear in the different statements of the original code.
The let instruction creates an instance of Persona, initializing its name. And the next statement calls the computed variable, which returns the greeting string and prints it:
Hola, me, llamo, Frodo
DSL in parameters
The specification of how to use the result builder attribute mentions, lastly, the possibility of using it on a closure parameter. Let us look at an example:
func imprimeSaludo(@StringConcatenator _ contenido: () -> String) {
let resultado = contenido()
print(resultado)
}
We are defining a function that will receive a parameterless closure returning a string. The body of the function executes the closure and prints the result. The @StringConcatenator annotation establishes that we will be able to pass DSL closures as arguments and that those closures will be transformed by the result builder.
In this way, we can call the previous function by using a closure in which we define the strings that will appear in the greeting. And we can do so without using the @StringConcatenator attribute explicitly, because it has already been defined on the function parameter:
imprimeSaludo {
"Hola"
"mundo"
}
The code above prints:
Hola, mundo
Let us look in more detail at how the example works. The imprimeSaludo function receives the contenido closure as a parameter. It is a parameterless closure that returns a string. And it is preceded by the @StringConcatenator attribute. This causes any argument that is passed, a closure returning a string, to be transformed by the result builder.
In the call to the function, we can see the Swift trailing-closure feature being used, which allows the parentheses to be omitted when the last argument is a closure.
The final code generated by the compiler is the following:
imprimeSaludo({
let v0 = "Hola"
let v1 = "mundo"
return StringConcatenator.buildBlock(v0, v1)
})
Obviously, this code is much less clear and direct than the previous one:
imprimeSaludo {
"Hola"
"mundo"
}
Advanced DSLs
In the examples above, we have seen how a DSL can be used to build a component from elementary components. But we have only seen a small part of everything that result builders make possible.
If we look at an advanced SwiftUI example, we will see that the result builder defined in SwiftUI, the ViewBuilder structure, allows a much more advanced DSL, one in which we can use loops, ForEach, and conditionals, if.
Example from the Hacking with Swift article List Items Inside if Statements:
struct TestView: View {
...
var body: some View {
List {
Button("Add a fresh potato") {
self.basket.vegetables.append(Vegetable(name: "Potato", freshness: 1))
}.foregroundColor(.blue)
Section(header: Text(sectionHeadings[0])) {
ForEach(self.basket.vegetables) { vegetable in
if vegetable.freshness == 0 {
Text(vegetable.name)
}
}
}
Section(header: Text(sectionHeadings[1])) {
ForEach(self.basket.vegetables) { vegetable in
if vegetable.freshness == 1 {
Text(vegetable.name)
}
}
}
}
}
}
In future posts we will continue exploring how result builders work and how to use them to construct this kind of powerful DSL.
References
- Proposal in Swift Evolution
- Introduction in the Swift Guide
- Detailed explanation in the Language Reference
- Code file with the examples from the post
Addendum (April 2025) - What has happened in Swift over these three years?
TL;DR
The basic ideas in the post are still correct, but Swift has removed several result builder limitations and has incorporated new, powerful macros that are worth knowing about. This appendix summarizes the relevant changes, Swift 5.7 through 5.10, while keeping the original post’s explanatory tone.
1. The end of the “limit of 10” thanks to parameter packs
In 2021, result builders internally generated a tuple of up to ten generics, hence the limitation mentioned in the post.
Since Swift 5.9, the compiler understands variadic generics, proposal SE-0390, and the standard library has rewritten ViewBuilder like this:
@resultBuilder
public enum ViewBuilder {
public static func buildBlock<each Content>(
_ components: repeat each Content
) -> TupleView<(repeat each Content)> where repeat each Content: View
}
Parameter packs, <each T> / repeat each T, delegate the arity to the compiler, so the SwiftUI DSL, and any builder that adopts that pattern, now accepts as many elements as you want, without manual overloads.
How to adapt it to your builders
Replace your oldstatic func buildBlock(_ parts: String...) -> Stringwith the modern variant:
static func buildBlock<each S>(_ parts: repeat each S) -> String where repeat each S == String
2. The new family of macros enters the scene
Swift 5.9 introduced compiler macros, SE-0389 and SE-0397. Although in the post we compared result builders with LISP/C macros, Swift’s native macros play in a different league:
| Feature | Result Builder | Macro |
|---|---|---|
Applied inside a body ({ ... }) |
Yes | Optional |
| Generates expressive code (views, HTML…) | Yes | Yes |
| Can create or alter complete declarations | No | Yes |
| Has access to the full AST | No, only its body | Yes |
| Invoked with an attribute | @MyBuilder |
@attachedMacro, #macro |
When to choose which
- Use result builders for purely declarative DSLs, SwiftUI, RegexBuilder, and the like.
- Choose macros for API generation, compile-time validations, or attributes such as
@Observable.
3. SwiftRegex is now part of the language
What was presented at WWDC22 as “SwiftRegex” became part of the standard syntax starting with Swift 5.7. Today you can write:
let fecha = "27/04/2025"
let patron = Regex(#"\d{2}/\d{2}/\d{4}"#)
if fecha ~= patron {
// ...
}
The underlying builder uses regular-expression components rather than a classic result builder, but your explanation of declarative DSLs remains fully valid.
4. Strict concurrency and asynchronous builders
Since Swift 5.10, Strict Concurrency mode is active by default.
If your builder generates async code:
@MyBuilder
func vista() async -> some View {
// ...
}
mark the buildBlock overloads with the appropriate async/throws, or the compiler will show warnings.
5. Other syntax details
- Partial inferences: you can declare
let saludo: _ = ...and let the builder resolve the type. buildPartialBlock: it allowsbuildBlock,buildEither, and related functions to be optional; the compiler synthesizes them if they are missing.- The builder attribute can now also be applied to initializers, which is very useful for creating complex objects declaratively.
To go deeper
- Proposal SE-0390 - Variadic Generics
- SE-0389 / SE-0397 - Swift Macros
- The Swift Programming Language -> Macros -> Result Builders
- WWDC23 video “Expand Swift macros”, which shows macros and builders working together