← All shorts
62Swift 5.4+2 min read

Result Builders: How @ViewBuilder Actually Works

@resultBuilder turns a sequence of statements into a single composed value, which is the mechanism SwiftUI's @ViewBuilder is built on.

@ViewBuilder looks like magic the first time you see a closure full of bare expressions turn into a view tree, but it's an ordinary language feature — @resultBuilder — that any type can opt into. The same mechanism works for building arrays, strings, or any other composed value.

#The old way

func makeChecklist(includeNotarization: Bool) -> [String] {
    var items: [String] = []
    items.append("Verify signature")
    items.append("Check entitlements")
    if includeNotarization {
        items.append("Run notarization")
    }
    for suffix in ["A", "B"] {
        items.append("Archive variant \(suffix)")
    }
    return items
}

#The new way

@resultBuilder
struct ArrayBuilder<Element> {
    static func buildBlock(_ components: [Element]...) -> [Element] {
        components.flatMap { $0 }
    }

    static func buildExpression(_ expression: Element) -> [Element] {
        [expression]
    }

    static func buildOptional(_ component: [Element]?) -> [Element] {
        component ?? []
    }

    static func buildEither(first component: [Element]) -> [Element] {
        component
    }

    static func buildEither(second component: [Element]) -> [Element] {
        component
    }

    static func buildArray(_ components: [[Element]]) -> [Element] {
        components.flatMap { $0 }
    }
}

func makeChecklist(includeNotarization: Bool, @ArrayBuilder<String> items: () -> [String]) -> [String] {
    items()
}

let checklist = makeChecklist(includeNotarization: true) {
    "Verify signature"
    "Check entitlements"
    if includeNotarization {
        "Run notarization"
    }
    for suffix in ["A", "B"] {
        "Archive variant \(suffix)"
    }
}

#Why it matters

  • Each bare statement in the closure becomes an expression the builder wraps via buildExpression — that's the same reason a Text("Hi") on its own line inside a SwiftUI body compiles
  • buildOptional and buildEither are what let plain if and if/else appear directly inside the closure without an explicit return
  • buildArray is what makes for loops work inside the builder — without it, a loop in the closure is a compile error
  • The builder type is completely generic here; @ViewBuilder is just ArrayBuilder's same set of static functions specialized for View instead of Element

#Gotcha

Every branch of an if/else or switch inside a result builder closure must produce the same Element type after building, because buildEither erases which branch ran into a single return type — mixing a String branch with an Int branch fails to compile with an error that points at the builder's static methods, not at the mismatched branch itself, which makes the actual mistake easy to miss on first read.

swiftconcurrency

Related shorts