iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…