What are result builders, and how does SwiftUI use them?
TL;DR
A result builder turns a block of plain statements into one value.
You write a type with @resultBuilder and static build... methods. The compiler calls them to combine each line.
SwiftUI's @ViewBuilder is a result builder. That is why you can list views without commas or return.
What it looks like from outside
VStack {
Text("Title")
if showSubtitle {
Text("Subtitle")
}
ForEach(items) { Text($0.name) }
}There is no array and no return. The @ViewBuilder on the VStack initializer makes this work.
Build a small one
Let us build a list of strings with the same style.
@resultBuilder
enum ListBuilder {
static func buildExpression(_ line: String) -> [String] { [line] }
static func buildBlock(_ parts: [String]...) -> [String] { parts.flatMap { $0 } }
static func buildOptional(_ part: [String]?) -> [String] { part ?? [] }
static func buildEither(first part: [String]) -> [String] { part }
static func buildEither(second part: [String]) -> [String] { part }
static func buildArray(_ parts: [[String]]) -> [String] { parts.flatMap { $0 } }
}
func makeMenu(@ListBuilder _ content: () -> [String]) -> [String] {
content()
}
let isPro = true
let menu = makeMenu {
"Home"
"Search"
if isPro {
"Analytics"
} else {
"Upgrade"
}
for i in 1...2 {
"Shortcut \(i)"
}
}
// ["Home", "Search", "Analytics", "Shortcut 1", "Shortcut 2"]What each method does
| Method | Used for |
|---|---|
buildExpression | Each single line. Converts it to a common type |
buildBlock | Combines all lines in a block. Required |
buildOptional | if without else |
buildEither(first:) / (second:) | if/else and switch |
buildArray | for ... in loops |
buildLimitedAvailability | if #available |
buildFinalResult | Final conversion of the whole result |
buildPartialBlock | Combine lines two at a time (instead of buildBlock) |
If you leave out a method, that syntax is not allowed in the builder. For example, without buildArray, a for loop is a compile error.
What the compiler does
Roughly, the closure above becomes:
let a = ListBuilder.buildExpression("Home")
let b = ListBuilder.buildExpression("Search")
let c: [String]
if isPro {
c = ListBuilder.buildEither(first: ListBuilder.buildBlock(ListBuilder.buildExpression("Analytics")))
} else {
c = ListBuilder.buildEither(second: ListBuilder.buildBlock(ListBuilder.buildExpression("Upgrade")))
}
// ... loop with buildArray ...
return ListBuilder.buildBlock(a, b, c /*, loop result */)It is all compile-time code rewriting. There is no runtime magic.
How @ViewBuilder keeps types
@ViewBuilder does not return an array. It keeps exact types:
- Several views become a
TupleView<(Text, Text)>. if/elsebecomes_ConditionalContent<A, B>.ifwithoutelsebecomesOptional<Text>.
These types are why body returns some View. Writing them by hand would be painful. They also help SwiftUI know the view structure and identity.
Older SwiftUI allowed at most 10 child views in one block. With parameter packs (Swift 5.9), that limit is gone.
Use @ViewBuilder in your own code
struct Card<Content: View>: View {
@ViewBuilder let content: Content
var body: some View {
VStack(alignment: .leading) { content }
.padding()
.background(.thinMaterial, in: .rect(cornerRadius: 12))
}
}
@ViewBuilder
func badge(for count: Int) -> some View {
if count > 0 {
Text("\(count)")
} else {
EmptyView()
}
}Adding @ViewBuilder to a function lets it return different view types from if/else.
Other real uses
- Regex builder (
RegexBuildermodule) for readable regular expressions. - Swift Charts chart content.
- HTML or SQL DSLs in server-side Swift.
Common follow-up questions
- Why can't you write
let x = 5in old builders? Older Swift did not allow declarations in builders. Since Swift 5.4, localletandvarare allowed. - Why does a
guardstatement not work inbody? Result builders do not supportguard,breakorcontinue. Useif/else. - What is
buildPartialBlockfor? It combines items one by one, so the builder supports any number of lines without manybuildBlockoverloads. - Is
@ViewBuilderapplied tobodyautomatically? Yes. TheViewprotocol marksbodywith@ViewBuilder. - Does a result builder cost performance at run time? No more than writing the calls by hand. The transformation happens at compile time.