Swift "@resultBuilder" — A Comprehensive Guide With Many Examples (2025)
Swift @resultBuilder — a comprehensive guide (2025)
A practical, copy‑pasteable handbook to designing, implementing, and using result builders to create expressive DSLs.
0) TL;DR
@resultBuilderlets you turn a multi‑statement closure into a single value at compile time.- You write a builder type (struct/enum/class) with static
build…methods. The compiler rewrites statements (if, loops, etc.) into calls to those methods. - Common methods:
buildBlockorbuildPartialBlock(first:/accumulated:next:), plusbuildExpression,buildEither,buildIf/buildOptional,buildArray,buildLimitedAvailability, and optionallybuildFinalResult. - Use it to build SwiftUI‑style APIs for HTML, SQL, Auto Layout, navigation graphs, app menus, regexes, charts…
- Keep builders small and composable to avoid type‑checking slow‑downs.
1) What is a result builder?
A result builder is a type annotated with @resultBuilder. When used to decorate a function/var/subscript parameter or return value, Swift rewrites the closure’s statements into calls on your builder’s static methods, producing one final value.
High‑level mental model
- Each statement in the closure becomes a component.
- Control flow (
if/else, loops) becomes calls to specific builder methods. - The builder then combines those components into a final result.
The transformation happens at compile time; there’s no runtime reflection or magic.
2) Where can I use @resultBuilder?
You can attach a result builder to:
- Function/initializer parameters
- Computed properties
- Subscripts
@resultBuilder
enum StringListBuilder {
static func buildExpression(_ expr: String) -> [String] { [expr] }
static func buildBlock(_ parts: [String]...) -> [String] { parts.flatMap { $0 } }
}
func makeCSV(@StringListBuilder _ content: () -> [String]) -> String {
content().joined(separator: ",")
}
let csv = makeCSV {
"apples"
"bananas"
"cherries"
}
// → "apples,bananas,cherries"
You can also return builder‑decorated content from computed properties:
struct Page {
@StringListBuilder var keywords: [String] {
"swift"; "result builders"; "dsl"
}
}
3) The builder methods — a quick reference
You don’t need all of these. Implement only what your DSL needs.
Statements in your closure → Methods you implement
─────────────────────────── ──────────────────────────────
A sequence of statements → buildBlock(_:) (or buildPartialBlock)
Single `if` (no else) → buildIf(_:) or buildOptional(_:) // either spelling works
`if` / `if-else` / `switch` → buildEither(first:) / buildEither(second:)
`for` loops / multiple elements → buildArray(_:) // receives [Component]
#available checks → buildLimitedAvailability(_:)
Coerce per-statement values → buildExpression(_:) // e.g. Int → [Int]
Convert final component type → buildFinalResult(_:) // optional
Modern, scalable statement chain → buildPartialBlock(first:) + buildPartialBlock(accumulated:next:)
Minimal “just works” set
For many builders:
buildExpression(_:)→ wraps a single element into your component type.buildBlock(_:)or the twobuildPartialBlockmethods → concatenates components.- Optionally add
buildEither/buildIfandbuildArrayto supportifandfor.
4) Classic vs modern concatenation
Historically, builders used buildBlock(_:) with several overloads or a variadic parameter:
static func buildBlock(_ parts: [Node]...) -> [Node] { parts.flatMap { $0 } }
Modern builders prefer buildPartialBlock(first:) and buildPartialBlock(accumulated:next:), which scale better with many statements and produce clearer type errors:
static func buildPartialBlock(first: [Node]) -> [Node] { first }
static func buildPartialBlock(accumulated: [Node], next: [Node]) -> [Node] { accumulated + next }
You can provide either approach; many Apple builders ship with
buildPartialBlocknow.
5) Worked example: a tiny HTML DSL
Let’s build tags and text nodes and render them to a String:
struct Node { let raw: String }
extension Node { static func text(_ s: String) -> Node { .init(raw: s) } }
@resultBuilder
enum HTMLBuilder {
static func buildExpression(_ n: Node) -> [Node] { [n] }
// Allow string literals directly
static func buildExpression(_ s: String) -> [Node] { [ .text(s) ] }
// Concatenate statements
static func buildPartialBlock(first: [Node]) -> [Node] { first }
static func buildPartialBlock(accumulated: [Node], next: [Node]) -> [Node] { accumulated + next }
// Conditionals & loops
static func buildEither(first c: [Node]) -> [Node] { c }
static func buildEither(second c: [Node]) -> [Node] { c }
static func buildIf(_ c: [Node]?) -> [Node] { c ?? [] }
static func buildArray(_ batches: [[Node]]) -> [Node] { batches.flatMap { $0 } }
}
struct Tag {
let name: String
@HTMLBuilder var children: [Node]
init(_ name: String, @HTMLBuilder @autoclosure children: () -> [Node]) { // autoclosure optional
self.name = name
self.children = children()
}
func render() -> String {
"<\(name)>\(children.map(\.raw).joined())</\(name)>"
}
}
func div(@HTMLBuilder _ content: () -> [Node]) -> Node { .init(raw: Tag("div", children: content()).render()) }
func span(_ text: String) -> Node { .init(raw: "<span>\(text)</span>") }
// Usage
let isAdmin = true
let items = ["Home", "Docs", "Pricing"]
let page = Tag("section", children: {
div {
"Welcome "
if isAdmin { span("(Admin)") }
}
for item in items { span(item) }
})
print(page.render())
// <section><div>Welcome <span>(Admin)</span></div><span>Home</span><span>Docs</span><span>Pricing</span></section>
Notes
buildExpressionlets us accept bothNodeandStringas statements.buildIfenables single‑branchifwithoutelse.buildArraypowersforloops by flattening anNode.
6) Worked example: Auto Layout constraint builder (UIKit)
A result builder can clean up a list of optional constraints:
@resultBuilder
enum ConstraintBuilder {
static func buildExpression(_ c: NSLayoutConstraint) -> [NSLayoutConstraint] { [c] }
static func buildBlock(_ parts: [NSLayoutConstraint]...) -> [NSLayoutConstraint] { parts.flatMap { $0 } }
static func buildEither(first c: [NSLayoutConstraint]) -> [NSLayoutConstraint] { c }
static func buildEither(second c: [NSLayoutConstraint]) -> [NSLayoutConstraint] { c }
static func buildOptional(_ c: [NSLayoutConstraint]?) -> [NSLayoutConstraint] { c ?? [] }
static func buildArray(_ batches: [[NSLayoutConstraint]]) -> [NSLayoutConstraint] { batches.flatMap { $0 } }
}
func constraints(@ConstraintBuilder _ content: () -> [NSLayoutConstraint]) -> [NSLayoutConstraint] { content() }
// Usage
let alignTop = true
let cs = constraints {
view.leadingAnchor.constraint(equalTo: container.leadingAnchor)
view.trailingAnchor.constraint(equalTo: container.trailingAnchor)
if alignTop {
view.topAnchor.constraint(equalTo: container.topAnchor)
}
}
NSLayoutConstraint.activate(cs)
7) Worked example: Query builder → SQL string
Showcases buildFinalResult to convert an array of clauses into a string:
@resultBuilder
enum SQLBuilder {
typealias Component = [String]
static func buildExpression(_ s: String) -> Component { [s] }
static func buildPartialBlock(first: Component) -> Component { first }
static func buildPartialBlock(accumulated: Component, next: Component) -> Component { accumulated + next }
static func buildArray(_ chunks: [Component]) -> Component { chunks.flatMap { $0 } }
static func buildEither(first c: Component) -> Component { c }
static func buildEither(second c: Component) -> Component { c }
// Final conversion to your API’s “real” type
static func buildFinalResult(_ component: Component) -> String {
component.joined(separator: " ")
}
}
func SELECT(@SQLBuilder _ content: () -> String) -> String { content() }
let adults = SELECT {
"SELECT name, age"
"FROM users"
"WHERE age >= 18"
}
// → "SELECT name, age FROM users WHERE age >= 18"
8) Control flow mapping (with code)
if / else
static func buildEither<T, F>(first: T) -> Either<T, F> { .left(T) }
static func buildEither<T, F>(second: F) -> Either<T, F> { .right(F) }
// Also provide buildIf(_:) *or* buildOptional(_:) for single-branch `if`.
Loops (for)
static func buildArray(_ components: [[Component]]) -> [Component] { components.flatMap { $0 } }
Availability (if #available)
static func buildLimitedAvailability(_ component: Component) -> Component { component }
You don’t call these yourself; the compiler lowers each construct to the appropriate method calls.
9) API design patterns
- Slot‑based containers: Accept multiple builder closures (
header,content,footer). - Composable decorators: Return a builder from extensions to conditionally apply transformations.
- Typed DSLs: Use generics to preserve static guarantees (e.g., valid HTML nesting, SQL injection‑safe literals).
- Erased DSLs: If you must mix heterogeneous types, provide overloads of
buildExpressionor (sparingly) type‑erase inbuildFinalResult.
10) Advanced techniques
- Overload
buildExpressionto accept rich statements: allowInt, domain types, or even closures that you invoke lazily. @autoclosureparameters: You can accept autoclosures in initializers/functions that forward into builder‑decorated storage.@escapingbuilders: If you store the closure for later, mark the parameter@escapingand consider capture lists to avoid cycles.- Custom
Componenttype: Define a lightweight internal component (e.g.,enum Clause { case select(String) ... }) and convert inbuildFinalResult. - Partial vs final result:
buildPartialBlockcan use a different accumulator type from your final result;buildFinalResultmaps it. - Testing DSLs: Make final results equatable (e.g., arrays/structs) so you can snapshot or assert on structure.
11) Interop notes with SwiftUI
SwiftUI’s @ViewBuilder is “just” a result builder specialized for views. You’ll see methods like buildIf(_:), buildEither, buildArray, and buildLimitedAvailability(_:) to support if, switch, loops, and availability checks. Many Apple frameworks now ship builders using buildPartialBlock instead of large buildBlock overload sets.
12) Pitfalls & best practices
- Prefer
buildPartialBlockfor long lists of statements; it scales and yields better errors. - Avoid heavy work inside the closure—precompute outside and pass the result in; keep the builder focused on structure, not computation.
- Stability of identity: If your DSL represents stateful elements (like UI views), be mindful of how conditional branches change structure.
- Explicit
returninside builder closures can change how type inference happens. Stick to plain statements where possible for the most ergonomic diagnostics. - Compilation time: Deeply nested, generic builders can slow type‑checking. Extract helpers, split blocks, or simplify generic interactions.
13) Recipe box (more ideas to try)
- Menu builder that composes
UIMenu/CommandMenuitems conditionally. - Navigation graph builder emitting a typed graph structure, then a router from it.
- Markdown builder turning nested elements into a string or attributed text.
- Regex builder (simplified) producing
Regexor NSRegularExpression with guarantees. - Feature‑flagged layout: one DSL that composes SwiftUI or UIKit depending on platform via availability.
14) Reference template — start here
Copy, rename, and adapt to your domain:
@resultBuilder
public enum Builder<Component> {
public static func buildExpression(_ expr: Component) -> [Component] { [expr] }
public static func buildPartialBlock(first: [Component]) -> [Component] { first }
public static func buildPartialBlock(accumulated: [Component], next: [Component]) -> [Component] { accumulated + next }
public static func buildEither(first c: [Component]) -> [Component] { c }
public static func buildEither(second c: [Component]) -> [Component] { c }
public static func buildIf(_ c: [Component]?) -> [Component] { c ?? [] }
public static func buildArray(_ batches: [[Component]]) -> [Component] { batches.flatMap { $0 } }
public static func buildLimitedAvailability(_ c: [Component]) -> [Component] { c }
// Optional: map accumulator → real API type
public static func buildFinalResult(_ c: [Component]) -> [Component] { c }
}
15) FAQ
Q: buildIf or buildOptional? Either works; choose one spelling and be consistent. Many Apple builders ship buildIf, while older examples show buildOptional.
Q: Do I still need buildBlock? No. Prefer the two buildPartialBlock methods unless you need backward compatibility with very old toolchains.
Q: Can a builder’s component type differ from the final result? Yes; use buildFinalResult for the conversion.
Q: How do switch statements work? The compiler lowers them to a series of buildEither(first:)/buildEither(second:) calls.
Q: Are result builders runtime‑expensive? No; they’re a compile‑time rewrite. Any overhead comes from the code you write and from compile‑time type‑checking, not from the builder mechanism itself.
Closing thought
Result builders are a lightweight way to make domain code read like the domain. Start with a tiny builder that only supports statements you need, then add conditionals, loops, and conversions as the DSL grows.