This article has been archived, as it was published several years ago, so some of its information might now be outdated. For more recent articles, please visit the main article feed.
A deep dive into Swiftâs result builders
Discover page available: SwiftUISwiftâs result builders feature is arguably one of the most interesting recent additions to the language, as it plays a core part in making SwiftUIâs declarative, DSL-like API work the way it does. In fact, result builders were first introduced as a semi-official language feature called âfunction buildersâ as part of the Swift 5.1 release that accompanied the introduction of SwiftUI, but has since been promoted into a proper part of the language as of Swift 5.4.
In this article, letâs take a closer look at how result builders work and how we can use them, as well as how they can give us some really valuable insights into how SwiftUIâs API operates under the hood.

Swift by Sundell is brought to you by the Genius Scan SDK â Add a powerful document scanner to any mobile app, and turn scans into high-quality PDFs with one line of code. Try it today.
Setting things up
For me, one of the best ways to truly understand how a given Swift feature works is to actually build something with it, so thatâs what weâll do. As an example, letâs say that weâre working on an app that includes an API for defining various settings â using a Setting type that looks like this:
struct Setting {
var name: String
var value: Value
}
extension Setting {
enum Value {
case bool(Bool)
case int(Int)
case string(String)
case group([Setting])
}
}The above example type uses associated enum values to ensure complete type safety even though various settings can contain different types of values. To learn more about that pattern, check out the Basics article about enums.
Since the above type includes support for nested settings (through its group value), weâre able to use it to construct hierarchies. For example, here weâve created a dedicated group for all of our settings that are considered experimental:
let settings = [
Setting(name: "Offline mode", value: .bool(false)),
Setting(name: "Search page size", value: .int(25)),
Setting(name: "Experimental", value: .group([
Setting(name: "Default name", value: .string("Untitled")),
Setting(name: "Fluid animations", value: .bool(true))
]))
]While thereâs certainly nothing really wrong with the above API (in fact, itâs quite nice!), letâs see what it could end up looking like if we were to give it a âresult builders makeoverâ â which in turn could let us transform it into more of a DSL, similar to what SwiftUI offers.
The basics of how result builders work
Like its name implies, Swiftâs result builders feature essentially lets us build a result by combining multiple expressions into a single value. Within SwiftUI, thatâs used to transform the contents of one of its many containers (such as HStack or VStack) into a single enclosing view, which can be seen by calling the type(of:) function on such a container instance:
import SwiftUI
let stack = VStack {
Text("Hello")
Text("World")
Button("I'm a button") {}
}
// Prints 'VStack<TupleView<(Text, Text, Button<Text>)>>'
print(type(of: stack))In general, anytime we see TupleView when using SwiftUI, that means that a result builder has been used to combine multiple views into one.
SwiftUI uses a number of different result builder implementations, such as ViewBuilder and SceneBuilder, but since weâre not able to look into the source code for those types, letâs instead build our own result builder for the settings API that we took a look at above.
Just like a property wrapper, a result builder is implemented as a normal Swift type thatâs annotated with a special attribute â @resultBuilder in this case. Then, specific method names are used to implement its various capabilities. For example, a method named buildBlock with zero arguments is used to build the result of an empty function or closure:
@resultBuilder
struct SettingsBuilder {
static func buildBlock() -> [Setting] { [] }
}The return type of the above function (an array of Setting values in our case) then determines the type of function or closure that our builder can be applied to. For example, we might choose to implement our top-level settings API as a global function that applies our new SettingsBuilder to any closure that was passed into it â like this:
func makeSettings(@SettingsBuilder _ content: () -> [Setting]) -> [Setting] {
content()
}With the above in place, we can now call makeSettings with an empty trailing closure and weâll get an empty array back:
let settings = makeSettings {}While our new API is not yet very useful, itâs already showed us a few aspects of how result builders work. But now, letâs actually start building some proper results.
Combining multiple values into a single result
To enable our SettingsBuilder to accept input, all that we have to do is to declare additional overloads of buildBlock with arguments matching the input that weâre looking to receive. In our case, weâll simply implement a single method that accepts a list of Setting values, which weâll then return as an array â like this:
extension SettingsBuilder {
static func buildBlock(_ settings: Setting...) -> [Setting] {
settings
}
}Above weâre using a variadic argument list, which SwiftUI canât currently use, since its View protocol contains an associated type. Instead, SwiftUIâs ViewBuilder defines 10 different overloads of buildBlock, each with a different number of arguments â which is why a SwiftUI view canât have more than 10 children. However, that limitation does not apply to our SettingsBuilder.
With that new buildBlock overload in place, weâll now be able to fill any closure that weâre passing to makeSettings with Setting values, and our result builder (with some help from the compiler) will combine all of those expressions into an array, which is then returned:
let settings = makeSettings {
Setting(name: "Offline mode", value: .bool(false))
Setting(name: "Search page size", value: .int(25))
Setting(name: "Experimental", value: .group([
Setting(name: "Default name", value: .string("Untitled")),
Setting(name: "Fluid animations", value: .bool(true))
]))
}While the above is arguably already a slight improvement over the inline array that we were previously using, letâs continue to take inspiration from SwiftUI, and also add a result builder-powered API for defining groups. To make that happen, letâs start by defining a new SettingsGroup type that also annotates a closure (this time stored in a property) with the @SettingsBuilder attribute in order to connect it to our result builder:
struct SettingsGroup {
var name: String
@SettingsBuilder var settings: () -> [Setting]
}An alternative approach wouldâve been to instead implement a custom initializer (rather than relying on Swiftâs memberwise initializers feature) and to then immediately call our settings closure and store its result, rather than storing a reference to the closure itself. That has the benefit of avoiding having to make our closure escaping at the cost of a slightly more verbose implementation that could prove to be a bit more performant, but also less flexible (as the closure will now only be called once, up front):
struct SettingsGroup {
var name: String
var settings: [Setting]
init(name: String,
@SettingsBuilder builder: () -> [Setting]) {
self.name = name
self.settings = builder()
}
}With either of the above two implementations in place (letâs go with the first one for now), weâre now able to define groups the exact same way as when defining top-level settings â by simply expressing each nested Setting within a closure, like this:
SettingsGroup(name: "Experimental") {
Setting(name: "Default name", value: .string("Untitled"))
Setting(name: "Fluid animations", value: .bool(true))
}However, if we actually try to place the above group within our makeSettings closure, weâll end up getting a compiler error â since our result builderâs buildBlock method currently expects a variadic list of Setting values, and our new SettingsGroup is a completely different type.
To fix that issue, letâs introduce a thin abstraction that can be shared between both Setting and SettingsGroup, for example in the shape of a protocol that lets us convert any instance of those types into an array of Setting values:
protocol SettingsConvertible {
func asSettings() -> [Setting]
}
extension Setting: SettingsConvertible {
func asSettings() -> [Setting] { [self] }
}
extension SettingsGroup: SettingsConvertible {
func asSettings() -> [Setting] {
[Setting(name: name, value: .group(settings()))]
}
}Then, we simply have to modify our result builderâs buildBlock implementation to accept SettingsConvertible instances, rather than concrete Setting values, and weâll then flatten that new argument list using flatMap:
extension SettingsBuilder {
static func buildBlock(_ values: SettingsConvertible...) -> [Setting] {
values.flatMap { $0.asSettings() }
}
}With the above in place, we can now define all of our settings in a very âSwiftUI-likeâ way, by constructing groups just like how weâd organize our various SwiftUI views using stacks and other containers:
let settings = makeSettings {
Setting(name: "Offline mode", value: .bool(false))
Setting(name: "Search page size", value: .int(25))
SettingsGroup(name: "Experimental") {
Setting(name: "Default name", value: .string("Untitled"))
Setting(name: "Fluid animations", value: .bool(true))
}
}Really nice! So the buildBlock overloads that a given result builder contains directly determines what type of expressions that weâll be able to place within each closure or function that has been annotated to use that builder.
Conditionals
Next, letâs take a look at how we can add support for evaluating conditionals within our result builder-powered closures. Initially, it might seem like that should âjust workâ, given that Swift itself supports all kinds of different conditionals. However, thatâs not the case â so with our current SettingsBuilder implementation weâll end up getting a compiler error if we try to do something like this:
let shouldShowExperimental: Bool = ...
let settings = makeSettings {
Setting(name: "Offline mode", value: .bool(false))
Setting(name: "Search page size", value: .int(25))
// Compiler error: Closure containing control flow statement
// cannot be used with result builder 'SettingsBuilder'.
if shouldShowExperimental {
SettingsGroup(name: "Experimental") {
Setting(name: "Default name", value: .string("Untitled"))
Setting(name: "Fluid animations", value: .bool(true))
}
}
}The above example once again shows us that the code thatâs being executed within a result builder-annotated closure isnât treated the same way as ânormalâ Swift code â as each expression needs to be explicitly handled by our builder, including conditionals like if statements.
To add that sort of handling code, weâll need to implement the buildIf method, which is what the compiler will map each stand-alone if statement to. Since each such statement can evaluate to either true or false, weâll get its body expression passed as an optional â which in our case will look like this:
// Here we extend Array to make it conform to our SettingsConvertible
// protocol, in order to be able to return an empty array from our
// 'buildIf' implementation in case a nil value was passed:
extension Array: SettingsConvertible where Element == Setting {
func asSettings() -> [Setting] { self }
}
extension SettingsBuilder {
static func buildIf(_ value: SettingsConvertible?) -> SettingsConvertible {
value ?? []
}
}With the above in place, our if statement from before now works just as weâd expect. But letâs also add support for combined if/else statements, which can be done by implementing two overloads of the buildEither method â one with the parameter label first, and one with second, each corresponding to the first and second branch of a given if/else statement:
extension SettingsBuilder {
static func buildEither(first: SettingsConvertible) -> SettingsConvertible {
first
}
static func buildEither(second: SettingsConvertible) -> SettingsConvertible {
second
}
}Weâll now be able to add an else clause to our if statement from before, for example in order to let users request access to our appâs experimental settings if those are not yet shown:
let settings = makeSettings {
Setting(name: "Offline mode", value: .bool(false))
Setting(name: "Search page size", value: .int(25))
if shouldShowExperimental {
SettingsGroup(name: "Experimental") {
Setting(name: "Default name", value: .string("Untitled"))
Setting(name: "Fluid animations", value: .bool(true))
}
} else {
Setting(name: "Request experimental access", value: .bool(false))
}
}Finally, those buildEither methods that we just implemented now (as of Swift 5.3) also enable switch statements to be used within result builder contexts, without requiring any additional build methods.
So for example, letâs say that weâre looking to refactor our above shouldShowExperimental boolean into an enum, in order to support multiple access levels. We could then simply switch on that enum within our makeSettings closure, and the Swift compiler will automatically route those expressions into our buildEither methods from before:
enum UserAccessLevel {
case restricted
case normal
case experimental
}
let accesssLevel: UserAccessLevel = ...
let settings = makeSettings {
Setting(name: "Offline mode", value: .bool(false))
Setting(name: "Search page size", value: .int(25))
switch accesssLevel {
case .restricted:
Setting.Empty()
case .normal:
Setting(name: "Request experimental access", value: .bool(false))
case .experimental:
SettingsGroup(name: "Experimental") {
Setting(name: "Default name", value: .string("Untitled"))
Setting(name: "Fluid animations", value: .bool(true))
}
}
}One additional thing worth noting about the above code is that weâre using a new Setting.Empty type within our switch statementâs .restricted case. Thatâs because weâre not (yet) able to use the break keyword within a result builder switch statement, so weâll need to express some kind of value within each code branch. So just like how SwiftUI has EmptyView, our new Settings API now has a Setting.Empty type for those kinds of situations:
extension Setting {
struct Empty: SettingsConvertible {
func asSettings() -> [Setting] { [] }
}
}And with that, our new result builder-powered settings API is now finished! Itâs really quite fascinating just how little code thatâs required to build a SwiftUI-like DSL using this new language feature.

Swift by Sundell is brought to you by the Genius Scan SDK â Add a powerful document scanner to any mobile app, and turn scans into high-quality PDFs with one line of code. Try it today.
Conclusion
With features like property wrappers and result builders, Swift is moving into some very interesting new territories, by enabling us to add our own logic to various fundamental language mechanisms â like how expressions are evaluated, or how properties are assigned and stored.
Granted, those new features do also make Swift more complicated, even though (at least in the best of worlds), they could also let library designers â both at Apple and in the wider developer community â hide that complexity behind well-formed APIs.
What do you think? Are you looking forward to using result builders within your own code, and did you gain some additional insight into how SwiftUIâs API works by reading this article? If so, feel free to share it, and youâre also more than welcome to contact me (either via Twitter or email) if you have any questions, comments, or feedback.
Thanks for reading! ð

