@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 aText("Hi")on its own line inside a SwiftUIbodycompiles buildOptionalandbuildEitherare what let plainifandif/elseappear directly inside the closure without an explicit returnbuildArrayis what makesforloops work inside the builder — without it, a loop in the closure is a compile error- The builder type is completely generic here;
@ViewBuilderis justArrayBuilder's same set of static functions specialized forViewinstead ofElement
#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.