How it works
The explanation, with examples you can run.
A small builder
@resultBuilderenum HTMLBuilder { static func buildBlock(_ parts: String...) -> String { parts.joined() } static func buildOptional(_ part: String?) -> String { part ?? "" } // if without else static func buildEither(first part: String) -> String { part } // if branch static func buildEither(second part: String) -> String { part } // else branch static func buildArray(_ parts: [String]) -> String { parts.joined() } // for-in} func ul(@HTMLBuilder _ content: () -> String) -> String { "<ul>\(content())</ul>" }func li(_ text: String) -> String { "<li>\(text)</li>" } let loggedIn = truelet items = ["Home", "Docs"] let html = ul { for item in items { li(item) } if loggedIn { li("Profile") } else { li("Sign in") } li("Help")}print(html) // <ul><li>Home</li><li>Docs</li><li>Profile</li><li>Help</li></ul>The rewrite
The compiler turns the closure body into roughly:
let v0 = HTMLBuilder.buildArray(items.map { item in HTMLBuilder.buildBlock(li(item)) })let v1: Stringif loggedIn { v1 = HTMLBuilder.buildEither(first: HTMLBuilder.buildBlock(li("Profile"))) }else { v1 = HTMLBuilder.buildEither(second: HTMLBuilder.buildBlock(li("Sign in"))) }let v2 = li("Help")return HTMLBuilder.buildBlock(v0, v1, v2)Statements the builder has no method for are rejected: without buildOptional, an if with no else is a compile error. let declarations are allowed and don't become components. buildExpression can convert each statement's value first, buildFinalResult the end result, and buildPartialBlock (Swift 5.7) lets a builder fold components two at a time instead of needing an overload per count.
What ViewBuilder does with types
ViewBuilder keeps the type of everything it combines. Three views in a block become a TupleView<(Text, Image, Button<Text>)>. An if/else becomes _ConditionalContent<A, B> through buildEither, which is why the two branches may contain different view types even though a normal function returning some View couldn't do that. An if without else becomes an Optional<A>, and ForEach handles loops (it is a view, not buildArray).
That full static type is how SwiftUI knows the structure of a view hierarchy: which parts can change and which stay the same, and so which views keep their identity and state. It is also why body is declared some View: the real type is enormous and changes with every edit. Wrapping branches in AnyView erases that information, so it is best kept for the rare places that need it.
Writing your own
Builders work for any type: SQL fragments, attributed strings, routing tables, test fixtures. Keep the component type simple, add only the control-flow methods you want to support, and remember that compile times grow with deeply nested builder expressions, because the compiler type-checks the whole rewritten body at once.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.