Swift Result Builders Cheat Sheet
Covers writing @resultBuilder types, buildBlock/buildOptional/buildEither methods, and how SwiftUI's ViewBuilder and DSLs like this work under the hood.
Defining a Result Builder
A result builder is a type with static `buildBlock`-family methods, applied via `@resultBuilder`.
@resultBuilderstruct HTMLBuilder { static func buildBlock(_ components: String...) -> String { components.joined(separator: "\n") }}@HTMLBuilderfunc page() -> String { "<h1>Title</h1>" "<p>Body text</p>"}print(page())// <h1>Title</h1>// <p>Body text</p>
Supporting `if`/`else` and Optionals
Implement `buildEither` and `buildOptional` to allow control flow inside the builder closure.
@resultBuilderstruct HTMLBuilder { static func buildBlock(_ components: String...) -> String { components.joined(separator: "\n") } static func buildEither(first component: String) -> String { component } static func buildEither(second component: String) -> String { component } static func buildOptional(_ component: String?) -> String { component ?? "" } static func buildArray(_ components: [String]) -> String { components.joined(separator: "\n") }}@HTMLBuilderfunc page(isAdmin: Bool) -> String { "<h1>Dashboard</h1>" if isAdmin { "<button>Admin Panel</button>" } else { "<p>Standard user</p>" } for i in 1...3 { "<li>Item \(i)</li>" }}
SwiftUI-style Usage
This is exactly the mechanism behind `@ViewBuilder`, `@SceneBuilder`, and `Regex` builders.
struct ContentView: View { var body: some View { VStack { // VStack's init takes @ViewBuilder content: () -> Content Text("Hello") if isLoggedIn { Text("Welcome back") } } }}// Applying a builder to a closure parameter:func container(@HTMLBuilder content: () -> String) -> String { "<div>\(content())</div>"}
Result Builder Static Methods
The methods the compiler looks for, and when each is invoked.
- buildBlock(_:)- combines a sequence of statements into one result (required)
- buildOptional(_:)- handles `if` without `else` (result is optional)
- buildEither(first:)/(second:)- handles `if/else` and `switch` branches
- buildArray(_:)- handles `for` loops inside the builder
- buildExpression(_:)- transforms each individual expression before blocking
- buildLimitedAvailability(_:)- handles `if #available` type erasure
- buildFinalResult(_:)- wraps/transforms the final built value before returning
buildPartialBlock for Heterogeneous Results
SwiftUI-style builders that mix many concrete View types use `buildPartialBlock` instead of a variadic `buildBlock`, avoiding the combinatorial overload explosion.
@resultBuilderstruct ViewLikeBuilder { static func buildPartialBlock<C>(first content: C) -> C { content } static func buildPartialBlock<C0, C1>( accumulated: C0, next: C1 ) -> TupleView2<C0, C1> { TupleView2(accumulated, next) }}struct TupleView2<A, B> { let a: A; let b: B init(_ a: A, _ b: B) { self.a = a; self.b = b }}// buildPartialBlock is folded pairwise left-to-right, which is how// SwiftUI supports 10+ children in a ViewBuilder without generating// buildBlock overloads for every arity up to 10.
buildExpression for Type Normalization
Implement `buildExpression` to accept several literal/expression types and coerce them into one internal representation.
@resultBuilderstruct HTMLBuilder { static func buildExpression(_ text: String) -> String { text } static func buildExpression(_ node: HTMLNode) -> String { node.rendered } static func buildBlock(_ components: String...) -> String { components.joined(separator: "\n") }}struct HTMLNode { let rendered: String }@HTMLBuilderfunc page() -> String { "<h1>Title</h1>" // hits the String overload HTMLNode(rendered: "<hr>") // hits the HTMLNode overload}
Custom DSL with buildLimitedAvailability
Handle `if #available` branches inside a result builder by erasing to a common type, mirroring how SwiftUI does availability-gated views.
@resultBuilderstruct ViewBuilderLike { static func buildEither<T, F>(first: T) -> _Either<T, F> { .left(first) } static func buildEither<T, F>(second: F) -> _Either<T, F> { .right(second) } static func buildLimitedAvailability<T>(_ component: T) -> AnyErasedView { AnyErasedView(component) }}enum _Either<L, R> { case left(L); case right(R) }struct AnyErasedView { init<T>(_ wrapped: T) {} }// Without buildLimitedAvailability, code inside `if #available { ... }`// would leak an opaque type the compiler can't unify across OS versions.
buildArray with Labeled `for` Loops and buildFinalResult
`buildArray` handles repeated content from loops; `buildFinalResult` post-processes the fully assembled tree once, useful for validation or sorting.
@resultBuilderstruct MenuBuilder { static func buildBlock(_ items: [String]...) -> [String] { items.flatMap { $0 } } static func buildExpression(_ item: String) -> [String] { [item] } static func buildArray(_ components: [[String]]) -> [String] { components.flatMap { $0 } } static func buildFinalResult(_ component: [String]) -> [String] { component.sorted() }}@MenuBuilderfunc menu(items: [String]) -> [String] { "Specials" for item in items { item }}// Result is always sorted because buildFinalResult runs once at the end.
Result Builders vs Hand-Rolled DSL Patterns
When a result builder pays off versus plain function chaining.
- Result builder- best when control flow (if/for/switch) needs to read naturally inside the block, e.g. SwiftUI bodies
- Method chaining / fluent API- simpler when there's no branching, just sequential configuration calls
- @resultBuilder on a closure param- the attribute goes on the parameter type, not the function, e.g. `func f(@Builder c: () -> T)`
- Type erasure cost- buildEither/buildOptional often force `AnyView`-style erasure, which has a real runtime diffing cost in SwiftUI
- Compile-time expansion- unlike macros, result builders are pure protocol/static-method dispatch, no SwiftSyntax plugin needed
- Debuggability- deeply nested buildPartialBlock chains produce very long inferred types, which can slow down type-checking on large bodies
Implement buildExpression(_:) to accept multiple input types (e.g. String and Image) and normalize them to one internal representation — that's how SwiftUI lets ViewBuilder mix wildly different View types in one block.