Result Builders
Result builders are the Swift feature behind SwiftUI’s declarative views: they let a closure return a hierarchy of values that the compiler turns into one composed result. Write your own and you can build DSLs that read like data — HTML, validation rules, attributed strings, regex.
@resultBuilder, SwiftUI, custom DSL
EXAMPLE
// 1) The mental model
// A closure with a @resultBuilder attribute returns NOT the last expression,
// but the result of buildBlock(...) called on every top-level statement.
//
// SwiftUI's @ViewBuilder is the most famous example — that's why View bodies
// can list multiple views without commas:
//
// var body: some View {
// VStack {
// Text("Hello")
// Text("World")
// }
// }
// 2) The smallest custom builder — string concatenation
@resultBuilder
struct StringBuilder {
static func buildBlock(_ parts: String...) -> String {
parts.joined(separator: " ")
}
}
func greet(@StringBuilder _ content: () -> String) -> String {
content()
}
let message = greet {
"Hello,"
"world!"
"Welcome."
}
// 'Hello, world! Welcome.'
// 3) Conditionals — buildOptional, buildEither
@resultBuilder
struct StringBuilder2 {
static func buildBlock(_ parts: String...) -> String { parts.joined(separator: " ") }
static func buildOptional(_ part: String?) -> String { part ?? "" }
static func buildEither(first: String) -> String { first }
static func buildEither(second: String) -> String { second }
}
func render(@StringBuilder2 _ content: () -> String) -> String { content() }
let isLoggedIn = true
let result = render {
"Welcome,"
if isLoggedIn {
"Mara!"
} else {
"guest."
}
"Today is nice."
}
// 4) Loops — buildArray
@resultBuilder
struct StringBuilder3 {
static func buildBlock(_ parts: String...) -> String { parts.joined(separator: " ") }
static func buildArray(_ components: [String]) -> String { components.joined(separator: " ") }
}
func list(@StringBuilder3 _ content: () -> String) -> String { content() }
let names = list {
for name in ["Mara", "Sam", "Alex"] {
"\(name),"
}
"and Kim."
}
// 5) The full method list — what makes a complete builder
// • buildBlock(_:) — combines top-level results
// • buildOptional(_:) — 'if' without 'else'
// • buildEither(first:) — 'if' branch
// • buildEither(second:) — 'else' branch
// • buildArray(_:) — 'for'
// • buildExpression(_:) — wraps raw expressions (lets you mix types)
// • buildFinalResult(_:) — final transform on the whole thing
// • buildLimitedAvailability — wraps 'if #available(...)' blocks
// 6) Real-world example — a tiny HTML DSL
indirect enum HTML {
case text(String)
case tag(name: String, attrs: [String: String], children: [HTML])
func render() -> String {
switch self {
case .text(let s): return s
case .tag(let name, let attrs, let children):
let a = attrs.isEmpty ? "" : " " + attrs.map { "\($0.key)=\"\($0.value)\"" }.joined(separator: " ")
let inner = children.map { $0.render() }.joined()
return "<\(name)\(a)>\(inner)</\(name)>"
}
}
}
@resultBuilder
struct HTMLBuilder {
static func buildBlock(_ children: HTML...) -> [HTML] { children }
static func buildExpression(_ s: String) -> HTML { .text(s) }
static func buildExpression(_ h: HTML) -> HTML { h }
static func buildOptional(_ child: [HTML]?) -> [HTML] { child ?? [] }
static func buildEither(first: [HTML]) -> [HTML] { first }
static func buildEither(second: [HTML]) -> [HTML] { second }
static func buildArray(_ components: [[HTML]]) -> [HTML] { components.flatMap { $0 } }
}
func tag(_ name: String, _ attrs: [String: String] = [:], @HTMLBuilder _ children: () -> [HTML]) -> HTML {
.tag(name: name, attrs: attrs, children: children())
}
let page = tag("html") {
tag("head") {
tag("title") { "Hello" }
}
tag("body", ["class": "home"]) {
tag("h1") { "Welcome" }
if isLoggedIn {
tag("p") { "Hi, Mara" }
} else {
tag("a", ["href": "/login"]) { "Sign in" }
}
for item in ["news", "about", "contact"] {
tag("a", ["href": "/\(item)"]) { item.capitalized }
}
}
}
print(page.render())
// 7) Validation DSL — collect errors
enum Validation {
case ok
case error(String)
}
@resultBuilder
struct ValidationBuilder {
static func buildBlock(_ rules: Validation...) -> [String] {
rules.compactMap { if case let .error(msg) = $0 { return msg } else { return nil } }
}
}
func validate(@ValidationBuilder _ rules: () -> [String]) -> [String] { rules() }
let errors = validate {
name.isEmpty ? .error("Name required") : .ok
email.contains("@") ? .ok : .error("Bad email")
password.count >= 8 ? .ok : .error("Password too short")
}
// 8) SwiftUI ViewBuilder
// @ViewBuilder is the framework's biggest builder. Most apps interact with it indirectly,
// but you can compose your own builder-returning functions:
func cardSection<Content: View>(@ViewBuilder _ content: () -> Content) -> some View {
VStack(alignment: .leading, spacing: 8, content: content)
.padding()
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 12))
}
// Use: cardSection { Text("A"); Text("B") }
// 9) Tradeoffs
// • Pros: declarative, terse, no imperative array-of-things plumbing
// • Cons: compile errors can be cryptic; advanced builders are harder to debug
// • Compile time: deeply nested builders slow Swift's type inference; split when type checker times out
// 10) Common bugs
// • Missing buildBlock for some arity → compile error
// • Conditional return type changes between branches → must implement buildEither pair
// • buildExpression overloads ambiguous → make types more specific
// • Mixing expression types without buildExpression → 'cannot convert value of type ... to ...'
// • Very long bodies (50+ rows) → split into smaller helpers; compiler chokes on type inference
// • Builder used outside its closure annotation — type system can't help; check parameter labels
Why it matters
Result builders turn a closure that lists values into a single composed result — SwiftUI’s @ViewBuilder writ small. Implement buildBlock, the optional / either / array helpers as needed, and you can build readable DSLs for HTML, validation, attributed strings, or whatever else benefits from declarative composition.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
@resultBuilder
struct StringBuilder {
static func buildBlock(_ parts: String...) -> String { parts.joined(separator: " ") }
}
@StringBuilder
func msg() -> String {
"Hello"; "world"
}
Try it Yourself »
Discussion
Loading…