Every method a builder can have — the statement that calls it, what it receives, the call written out — and what SwiftUI's own builder leaves out
Lesson 4 builds WidgetBuilder with the methods TinyUI needs. A result builder can have more, and this guide goes through all of them using one builder, Words, which turns a block of words into a sentence. For each method: the statement that makes the compiler call it, what it receives and what it returns, and the same call written out by hand, run side by side with the sugar. Partway through, a look inside the compiler itself: what it prints when it has rewritten a for loop, the C++ that does the rewriting, and what is left of it in SIL. After that comes SwiftUI's own builder — which of the methods it has, which it leaves out, and why a for loop is the one statement a SwiftUI body cannot hold. Every piece of code on this page was compiled and run on the iOS 27 simulator, and every output, error and note shown is what that run really gave.
buildExpression — every value, first · lesson 4buildBlock — one call per block · lesson 4buildPartialBlock — the same job, two at a time · lesson 4buildOptional — if without else, and if let · lesson 4buildEither — if/else and switch · lesson 4buildArray — for … in · lesson 4for loop is taken apart · lesson 4buildLimitedAvailability — if #available · lesson 4buildFinalResult — once, at the end · lesson 4ViewBuilder has no buildArray · lesson 4A result builder is any type marked @resultBuilder. Its methods are static, and the compiler finds them by name — there is no protocol to conform to, just as there is none for property wrappers (lesson 2). The value the methods pass between them is called the builder's component. For Words it is [String]: the words so far.
@resultBuilder
enum Words {
static func buildBlock(_ parts: [String]...) -> [String] {
parts.flatMap { $0 }
}
static func buildExpression(_ word: String) -> [String] {
[word]
}
static func buildExpression(_ number: Int) -> [String] {
[String(number)]
}
static func buildOptional(_ part: [String]?) -> [String] {
part ?? []
}
static func buildEither(first: [String]) -> [String] {
first
}
static func buildEither(second: [String]) -> [String] {
second
}
static func buildArray(_ parts: [[String]]) -> [String] {
parts.flatMap { $0 }
}
static func buildLimitedAvailability(_ part: [String]) -> [String] {
part
}
static func buildFinalResult(_ parts: [String]) -> String {
parts.joined(separator: " ")
}
}| You write | The compiler calls | In Words |
|---|---|---|
| An expression on a line of its own | buildExpression | a String or an Int becomes [String] |
Several statements in one block — any { } | buildBlock, once for the block (or buildPartialBlock, one statement at a time) | joins the parts |
if without else, and if let | buildOptional | nothing becomes [] |
if/else, and switch | buildEither(first:) or buildEither(second:) | passes the branch on |
for … in | buildArray, once, after the loop | joins every pass |
if #available | buildLimitedAvailability | passes it on |
| Nothing — it runs once, at the very end | buildFinalResult | turns [String] into one String |
Mark a function with the builder's name and its body is built with it. This one uses every method:
@Words
func sentence(excited: Bool, nickname: String?, places: [String]) -> String {
"Pip"
if let nickname {
"(\(nickname))"
}
"went"
for place in places {
"to"
place
}
3
"times"
if excited {
"!"
} else {
"."
}
if #available(iOS 27, *) {
"today"
}
}Called once with everything and once with nothing optional, it prints
Pip (Biscuit) went to Seattle to Victoria 3 times ! today
Pip went 3 times . todayTo watch the calls, give each method a print. Traced is Words with one added line in every method (and without the Int overload, which the block below does not need):
@resultBuilder
enum Traced {
static func buildBlock(_ parts: [String]...) -> [String] {
print("buildBlock(\(parts))")
return parts.flatMap { $0 }
}
static func buildExpression(_ word: String) -> [String] {
print("buildExpression(\"\(word)\")")
return [word]
}
static func buildOptional(_ part: [String]?) -> [String] {
print("buildOptional(\(part.map { "\($0)" } ?? "nil"))")
return part ?? []
}
static func buildEither(first: [String]) -> [String] {
print("buildEither(first: \(first))")
return first
}
static func buildEither(second: [String]) -> [String] {
print("buildEither(second: \(second))")
return second
}
static func buildArray(_ parts: [[String]]) -> [String] {
print("buildArray(\(parts))")
return parts.flatMap { $0 }
}
static func buildLimitedAvailability(_ part: [String]) -> [String] {
print("buildLimitedAvailability(\(part))")
return part
}
static func buildFinalResult(_ parts: [String]) -> String {
print("buildFinalResult(\(parts))")
return parts.joined(separator: " ")
}
}@Traced
func tracedSentence(excited: Bool) -> String {
"Pip"
if excited {
"!"
}
for place in ["Seattle", "Victoria"] {
place
}
if #available(iOS 27, *) {
"today"
} else {
"some day"
}
}tracedSentence(excited: false) prints
buildExpression("Pip")
buildOptional(nil)
buildExpression("Seattle")
buildBlock([["Seattle"]])
buildExpression("Victoria")
buildBlock([["Victoria"]])
buildArray([["Seattle"], ["Victoria"]])
buildExpression("today")
buildBlock([["today"]])
buildLimitedAvailability(["today"])
buildEither(first: ["today"])
buildBlock([["Pip"], [], ["Seattle", "Victoria"], ["today"]])
buildFinalResult(["Pip", "Seattle", "Victoria", "today"])
= Pip Seattle Victoria todayRead it from the top:
Each expression goes through buildExpression when the code reaches it.
The if is false, so buildOptional receives nil — and "!" never reached buildExpression. The inside of a false if is not run at all.
Each pass through the loop is a block of its own, so buildBlock runs once per pass. buildArray runs once, when the loop has finished, with every pass in hand.
The #available branch is a block too; its result then goes through buildLimitedAvailability, and only then through buildEither.
The function's own block is combined last, once everything in it is ready — and buildFinalResult comes after that, once.
buildExpression — every value, firstEvery expression that stands on a line of its own is handed to buildExpression before anything else happens to it, and what comes back is a component. This is where a builder decides which values it accepts. Words has two overloads, one for String and one for Int, so a number is welcome in its blocks:
@Words
func count() -> String {
"Pip went"
3
"times"
}func countByHand() -> String {
Words.buildFinalResult(
Words.buildBlock(
Words.buildExpression("Pip went"),
Words.buildExpression(3),
Words.buildExpression("times")
)
)
}Both print
Pip went 3 times
Pip went 3 timesWithout an overload for a value's type, the value is refused. Plain accepts String only:
@resultBuilder
enum Plain {
static func buildBlock(_ parts: [String]...) -> [String] {
parts.flatMap { $0 }
}
static func buildExpression(_ word: String) -> [String] {
[word]
}
}@Plain
func countPlainly() -> [String] {
"Pip went"
3
"times"
}It does not compile
cannot convert value of type 'Int' to expected argument type 'String'
buildExpression is optional. Without it, each expression must already be a component, and goes to buildBlock exactly as it is. SwiftUI's builder has none — every line of a body goes to its buildBlock as it stands.
buildBlock — one call per blockbuildBlock receives all the components of one block at once, in order — which is why Words declares it variadic, [String].... It is called once for every block: the function's own body, each branch of an if, each pass through a loop. In the trace in Every call, in order it ran four times: once for each of the loop's two passes, once for the #available branch, and last of all once for the function itself.
An empty block calls it with no arguments. A variadic buildBlock covers that case for free — Words.buildBlock() is [] — while SwiftUI declares a separate buildBlock() for it, which returns EmptyContent (see SwiftUI's builder, below).
buildPartialBlock — the same job, two at a timeInstead of buildBlock, a builder can declare a pair of methods that combine a block one statement at a time: buildPartialBlock(first:) for the first component, and buildPartialBlock(accumulated:next:) to fold in each one after it. Lesson 4's WidgetBuilder uses this pair, because each step returns a bigger type — Column<A, B> — and a plain variadic method cannot do that: [String]... needs every part to be the same type. (SwiftUI's buildBlock gets round that another way, with parameter packs — repeat each Content.)
A builder may declare both. The compiler then uses the pair:
@resultBuilder
enum Both {
static func buildBlock(_ parts: String...) -> String {
print("buildBlock(\(parts))")
return parts.joined(separator: " ")
}
static func buildPartialBlock(first: String) -> String {
print("buildPartialBlock(first: \"\(first)\")")
return first
}
static func buildPartialBlock(accumulated: String, next: String) -> String {
print("buildPartialBlock(accumulated: \"\(accumulated)\", next: \"\(next)\")")
return accumulated + " " + next
}
}
@Both
func three() -> String {
"Pip"
"went"
"home"
}three() prints
buildPartialBlock(first: "Pip")
buildPartialBlock(accumulated: "Pip", next: "went")
buildPartialBlock(accumulated: "Pip went", next: "home")
= Pip went homeSE-0348, the proposal that added the pair, says the transform will "always prefer buildPartialBlock to buildBlock when they are both defined".
buildOptional — if without else, and if letAn if without an else either produces a component or it does not. If the condition holds, its block is built and buildOptional receives the result; if not, buildOptional receives nil. if let works the same way.
@Words
func greeting(nickname: String?) -> String {
"Pip"
if let nickname {
nickname
}
}func greetingByHand(nickname: String?) -> String {
var middle: [String]? = nil
if let nickname {
middle = Words.buildBlock(Words.buildExpression(nickname))
}
return Words.buildFinalResult(
Words.buildBlock(
Words.buildExpression("Pip"),
Words.buildOptional(middle)
)
)
}With a nickname and without, both print
Pip Biscuit | Pip Biscuit
Pip | PipThe place for the optional part is always there, filled or empty. In SwiftUI that place is an Optional<Text> in the view's type (lesson 4, step 10), which is how a view keeps its position while a condition changes.
SwiftUI's builder spells this method buildIf, the older name. The compiler still looks for it: its source says "Use buildOptional(_:) if available, otherwise fall back to buildIf".
Apple's documentation of ForEach adds a warning about this method. An if without else in a row means that row produces one view or none, and lazy containers such as List are fastest when every row produces the same number. The fix it gives is to wrap the if in a VStack, and the launch argument -LogForEachSlowPath YES makes SwiftUI log the rows that do not.
buildEither — if/else and switchWith an else, exactly one branch runs. Its block's result goes to buildEither(first:) if it was the first branch, and to buildEither(second:) if it was the second — so both must end up as the same type.
@Words
func ending(excited: Bool) -> String {
"Pip went home"
if excited {
"!"
} else {
"."
}
}func endingByHand(excited: Bool) -> String {
let last: [String]
if excited {
last = Words.buildEither(first: Words.buildBlock(Words.buildExpression("!")))
} else {
last = Words.buildEither(second: Words.buildBlock(Words.buildExpression(".")))
}
return Words.buildFinalResult(
Words.buildBlock(
Words.buildExpression("Pip went home"),
last
)
)
}Excited and not, both print
Pip went home ! | Pip went home !
Pip went home . | Pip went home .There are only two methods, so a switch with three cases — or an if/else if/else — is built as a tree of two-way choices. Traced shows its shape:
@Traced
func mood(_ n: Int) -> String {
switch n {
case 0:
"calm"
case 1:
"busy"
default:
"sleepy"
}
}Each case, traced
-- mood(0)
buildExpression("calm")
buildBlock([["calm"]])
buildEither(first: ["calm"])
buildEither(first: ["calm"])
buildBlock([["calm"]])
buildFinalResult(["calm"])
= calm
-- mood(1)
buildExpression("busy")
buildBlock([["busy"]])
buildEither(second: ["busy"])
buildEither(first: ["busy"])
buildBlock([["busy"]])
buildFinalResult(["busy"])
= busy
-- mood(2)
buildExpression("sleepy")
buildBlock([["sleepy"]])
buildEither(second: ["sleepy"])
buildBlock([["sleepy"]])
buildFinalResult(["sleepy"])
= sleepy"calm" is the first of the first, "busy" the second of the first, "sleepy" the second. SE-0289 calls it a "balanced binary tree" of injections. SwiftUI's buildEither returns _ConditionalContent<TrueContent, FalseContent>, and three branches nest into it in exactly this shape — lesson 4's last edge case shows the type.
buildArray — for … inThe body of a loop is a block, built once per pass. Each pass's result is kept, and when the loop has finished, buildArray receives them all as one array.
@Words
func route(_ places: [String]) -> String {
"Pip went"
for place in places {
"to"
place
}
}func routeByHand(_ places: [String]) -> String {
var passes: [[String]] = []
for place in places {
passes.append(Words.buildBlock(Words.buildExpression("to"), Words.buildExpression(place)))
}
return Words.buildFinalResult(
Words.buildBlock(
Words.buildExpression("Pip went"),
Words.buildArray(passes)
)
)
}Both print
Pip went to Seattle to Victoria
Pip went to Seattle to VictoriaNote when buildArray is called: after the loop. By then every pass has run and everything it made already exists. That timing is half of the reason SwiftUI's builder has no buildArray — see Why ViewBuilder has no buildArray, at the end.
Leave buildArray out, and the compiler says which method to add:
@Plain
func placesPlainly() -> [String] {
"Pip went"
for place in ["to", "Seattle"] {
place
}
}It does not compile
closure containing control flow statement cannot be used with result builder 'Plain'
note: add 'buildArray(_:)' to the result builder 'Plain' to add support for 'for'..'in' loops
for loop is taken apartNone of this happens while your program runs. The compiler does the rewriting — in its type checker, written in C++ — before your code becomes anything else, and its -print-ast option prints a function as it was rewritten. Here is route from the section above:
@Words @MainActor @preconcurrency internal func route(_ places: [String]) -> String {
var $__builder0: [String] = Words.buildExpression("Pip went")
var $__builder1: [String]
do {
var $__builder2: [[String]] = []
for place in places {
var $__builder3: [String] = Words.buildExpression("to")
var $__builder4: [String] = Words.buildExpression(place)
$__builder2.append(Words.buildBlock($__builder3, $__builder4))
}
$__builder1 = Words.buildArray($__builder2)
}
return Words.buildFinalResult(Words.buildBlock($__builder0, $__builder1))
}The loop is still there, but taken apart. Before it, a hidden array, [[String]], with room for one component per pass. Inside it, the body's two statements go through buildExpression, buildBlock combines them, and the result is appended to the array — the body itself no longer produces anything. After it, buildArray receives the whole array at once. Last, the function's own block, and buildFinalResult.
The code that does this is visitForEachStmt, in the compiler's lib/Sema/BuilderTransform.cpp. Its own comment draws the shape it produces, and its first lines are where the "add 'buildArray(_:)'" note above comes from:
/// do {
/// var $__forEach = []
/// for ... in ... {
/// ...
/// $__builderVar = buildBlock(...)
/// $__forEach.append($__builderVar)
/// }
/// buildArray($__forEach)
/// }
NullablePtr<Stmt> visitForEachStmt(ForEachStmt *forEachStmt,
NullablePtr<VarDecl> forEachVar) {
// for...in statements are handled via buildArray(_:); bail out if the
// builder does not support it.
if (!builder.supports(ctx.Id_buildArray))
return failTransform(forEachStmt);
…
SourceLoc startLoc = forEachStmt->getStartLoc();
SourceLoc endLoc = forEachStmt->getEndLoc();
…
// Form a call to Array.append(_:) to add the result of executing each
// iteration of the loop body to the array formed above.
{
auto arrayVarRef = builder.buildVarRef(arrayVar, endLoc);
auto arrayAppendRef = new (ctx) UnresolvedDotExpr(
arrayVarRef, endLoc, DeclNameRef(ctx.getIdentifier("append")),
DeclNameLoc(endLoc), /*implicit=*/true);
…
// $__forEach = buildArray($__arrayVar)
doBody.push_back(buildAssignment(
forEachVar.get(),
builder.buildCall(forEachStmt->getEndLoc(), ctx.Id_buildArray,
{builder.buildVarRef(arrayVar, endLoc)},
{Identifier()})));
…
return DoStmt::createImplicit(ctx, LabeledStmtInfo(), doBody);
}Between those two pieces it works in syntax trees, not text: it creates the empty array (ArrayExpr::create), rewrites the loop's body as a block of its own (transform(forEachStmt->getBody(), …)), adds a call to the array's append at the end of that body (UnresolvedDotExpr(…, "append")), and after the loop assigns the result of buildArray — all inside an implicit do { }.
By the time the compiler lowers the function to SIL, its intermediate language, there is no builder left — only a loop and function calls. And SIL remembers which line of your source each instruction came from. Here is route line by line, each line followed by the functions that its instructions call:
route in SIL: each source line, and what the instructions charged to it call
Read it from the top. The function's first line carries its own buildBlock and buildFinalResult — the last things to run. for place in places { became the hidden array, the iterator (makeIterator, then next() on every pass) and the buildBlock that closes each pass. "to" and place became buildExpression calls. (The _allocateUninitializedArray and _finalizeUninitializedArray under "Pip went" and "to" build the array in which a variadic buildBlock receives its arguments.) And the loop's closing brace, a line with no code on it at all, became the append and the buildArray: exactly where the C++ above put them, at endLoc, the end of the loop. The loop has been taken apart, and its pieces filed under the lines that made them. To SIL, buildArray is just one more call to a static function.
buildLimitedAvailability — if #availableAn if #available branch is built like any other if, with one extra step: its block's result goes through buildLimitedAvailability before it reaches buildEither or buildOptional. The reason is the type. A branch may use a type that does not exist on older systems, and if that type became part of the block's result type, the function could not be compiled for them at all. This method is the builder's chance to hide it.
@Words
func when() -> String {
"Pip went"
if #available(iOS 27, *) {
"today"
} else {
"some day"
}
}func whenByHand() -> String {
let part: [String]
if #available(iOS 27, *) {
part = Words.buildEither(first: Words.buildLimitedAvailability(Words.buildBlock(Words.buildExpression("today"))))
} else {
part = Words.buildEither(second: Words.buildBlock(Words.buildExpression("some day")))
}
return Words.buildFinalResult(
Words.buildBlock(
Words.buildExpression("Pip went"),
part
)
)
}Both print
Pip went today
Pip went todayWords has nothing to hide — both branches are [String] — so it passes the value on. SwiftUI's version returns AnyView: it erases the type, the one place where SwiftUI puts an AnyView into your view without being asked. Lesson 4's edge case 7 shows it happening.
buildFinalResult — once, at the endEvery other method passes components around. buildFinalResult is called once, on the outermost block's result, and whatever it returns is what the function returns — which may be another type. Words works in [String] and finishes with one String, so every function built with it returns String. Declare one as returning [String], and the compiler points at the mismatch:
@Words
func listed() -> [String] {
"Pip"
"went"
}It does not compile
cannot convert return expression of type 'String' to return type '[String]'
Without buildFinalResult, the outermost block's component is returned as it is. SwiftUI's builder has none: a body returns exactly what its block built.
The builder's name is an attribute that switches the transformation on for one body. Besides a function, it can go on a computed property, on a closure parameter, and on a protocol requirement:
@Words
var greetingLine: String {
"Hello"
"Pip"
}
func say(@Words _ make: () -> String) -> String {
make()
}
protocol Speaker {
@Words var line: String { get }
}
struct Dog: Speaker {
var line: String {
"woof"
2
}
}Using each of them prints
Hello Pip
Pip sat
woof 2On a parameter, the attribute applies to the closure the caller writes — that is how VStack { … } works. On a protocol requirement it is inherited: Dog.line carries no attribute, yet it is built with Words. That is why you never write @ViewBuilder on a SwiftUI body: the View protocol's requirement carries it.
Methods decide which statements a builder accepts — but some statements no method can make acceptable. Right below visitForEachStmt, the same file lists the statements the transformation has no translation for:
UNSUPPORTED_STMT(Throw)
UNSUPPORTED_STMT(Return)
UNSUPPORTED_STMT(Yield)
UNSUPPORTED_STMT(Then)
UNSUPPORTED_STMT(Discard)
UNSUPPORTED_STMT(Defer)
UNSUPPORTED_STMT(Guard)
UNSUPPORTED_STMT(While)
UNSUPPORTED_STMT(DoCatch)
UNSUPPORTED_STMT(RepeatWhile)
UNSUPPORTED_STMT(Break)
UNSUPPORTED_STMT(Continue)A block that holds one of these cannot be built. Lesson 4's cards and lesson 4's edge cases show what that means in practice. A while loop does not compile, whatever methods the builder has, and neither does do/catch. An explicit return is the odd one: it does not fail on its own — it switches the builder off for the whole block, which is then type-checked as ordinary code. That is why a guard, which must leave with a return, goes wrong in a body (lesson 4, edge case 3). Declarations are a different matter: a let in a builder block is left alone, so let _ = print(…) is the way to put a side effect in a body.
SwiftUI's builder is ViewBuilder. Xcode 27 also calls it ContentBuilder — a typealias for the same type, the name reflecting that the builder now builds toolbar items and menu commands as well as views. These are the methods the iOS 27 SDK declares on it, from SwiftUICore's interface, with their bodies and attributes left out:
@_functionBuilder public struct ViewBuilder {
static func buildBlock() -> EmptyContent
static func buildBlock<Content>(_ content: Content) -> Content
static func buildBlock<each Content>(_ content: repeat each Content) -> TupleView<(repeat each Content)>
where repeat each Content : View
@available(iOS 27.0, *)
static func buildBlock<each Content>(_ content: repeat each Content) -> TupleContent<repeat each Content>
}
extension ViewBuilder {
static func buildIf<Content>(_ content: Content?) -> Content?
static func buildEither<TrueContent, FalseContent>(first: TrueContent) -> _ConditionalContent<TrueContent, FalseContent>
static func buildEither<TrueContent, FalseContent>(second: FalseContent) -> _ConditionalContent<TrueContent, FalseContent>
static func buildLimitedAvailability<Content>(_ content: Content) -> AnyView
}
public typealias ContentBuilder = ViewBuilderNote the attribute: @_functionBuilder, not @resultBuilder. That is the underscored name the feature had when SwiftUI shipped it in 2019; SE-0289 made it public as @resultBuilder in Swift 5.4, two years later, and SwiftUI's declaration has kept the old spelling, which the compiler still accepts. Set against the methods above:
| Method | Words | Lesson 4's WidgetBuilder | SwiftUI's ViewBuilder |
|---|---|---|---|
buildExpression | Yes | No | No — each line goes to buildBlock as it stands |
buildBlock | Yes | No | Yes: for an empty block, one statement, and any number (parameter packs) |
buildPartialBlock | No | Yes | No |
buildOptional | Yes | Yes | Yes, under its older name, buildIf |
buildEither | Yes | Yes | Yes: _ConditionalContent |
buildArray | Yes | Yes, until step 8 | No |
buildLimitedAvailability | Yes | No | Yes: it returns AnyView |
buildFinalResult | Yes | No | No |
The SwiftUI module adds more overloads of three of those names — buildBlock, buildIf and buildLimitedAvailability — for toolbar content, menu commands and accessibility rotors. None of them adds a method the table says is missing.
ViewBuilder has no buildArrayWhat it means. A for loop cannot appear in a body, or in any other @ViewBuilder block:
import SwiftUI
struct StopList: View {
let stops: [String]
var body: some View {
VStack(alignment: .leading) {
for stop in stops {
Text("- " + stop)
}
}
}
}It does not compile
closure containing control flow statement cannot be used with result builder 'ViewBuilder'
In SwiftUI a loop is a view instead: ForEach, which takes the collection and a closure that makes one row. An if or a switch inside that closure is fine. Lesson 4's LoopEach is the same thing for TinyUI.
It is a choice, not a limit. One more static method would make for work, and SwiftUI's builder for keyframe animations, KeyframeTrackContentBuilder, declares exactly that method — so for is allowed inside a KeyframeTrack:
public static func buildArray(_ components: [some KeyframeTrackContent<Value>]) -> some KeyframeTrackContent<Value>Keyframes are made once, in order, and that is all that is ever done with them. Views are not, and the difference explains the omission. Apple's documentation does not give a reason in so many words, but it describes ForEach as a structure that "computes views on demand" from "identified data" — and those are the two things a buildArray cannot give.
On demand. buildArray is called after the loop has finished, with every pass already run — Every call, in order traced it, and Inside the compiler shows the code that does it. So every row would exist, all of them, every time body runs. ForEach keeps the collection and the closure instead, and the container decides which rows to make: a List of a thousand rows makes only the rows on the screen.
Identified. SwiftUI runs body again whenever something it reads changes, and matches each new row to an old one, to know what moved, what was inserted and which row keeps which state. An array from buildArray carries nothing but the rows, so the only thing that could tell two rows apart is their position. ForEach will not even compile without an identity: its elements must be Identifiable, or you pass id:.
Both are run on the simulator — a List of a thousand rows counting what it makes, and a city inserted at the top of a list identified by position and by name — in the guide ForEach, in full.
10 cards, for this guide only: they are not in lesson 4's deck. Answer in your head first, then tap the card.
Each expression: buildExpression. Each block: buildBlock. if without else and if let: buildOptional. if/else and switch: buildEither. for: buildArray. if #available: buildLimitedAvailability. Once at the end: buildFinalResult. This function uses them all and printed: Pip (Biscuit) went to Seattle to Victoria 3 times ! today
Pip went 3 times . today
if whose condition is false. Does the builder ever see the statements inside it?No. The block inside a false if is not run: buildOptional receives nil, and the "!" never reaches buildExpression. Every call, in order: buildExpression("Pip")
buildOptional(nil)
buildExpression("Seattle")
buildBlock([["Seattle"]])
buildExpression("Victoria")
buildBlock([["Victoria"]])
buildArray([["Seattle"], ["Victoria"]])
buildExpression("today")
buildBlock([["today"]])
buildLimitedAvailability(["today"])
buildEither(first: ["today"])
buildBlock([["Pip"], [], ["Seattle", "Victoria"], ["today"]])
buildFinalResult(["Pip", "Seattle", "Victoria", "today"])
= Pip Seattle Victoria today
[String] accept an Int on a line of its own.Give it a buildExpression overload for Int. Every expression passes through buildExpression first; the overload for its type turns it into a component. The sugar and the calls written out both printed: Pip went 3 times
Pip went 3 times
buildBlock and also the buildPartialBlock pair. Which does the compiler call?The buildPartialBlock pair — always. buildBlock is never called. SE-0348: the transform will "always prefer buildPartialBlock to buildBlock" when both are defined. It printed: buildPartialBlock(first: "Pip")
buildPartialBlock(accumulated: "Pip", next: "went")
buildPartialBlock(accumulated: "Pip went", next: "home")
= Pip went home
if let nickname { nickname } becomes in a builder block.Build the branch's block only if the condition holds, keep nil otherwise, and pass the optional to buildOptional. Sugar and hand-written, with a nickname and without: Pip Biscuit | Pip Biscuit
Pip | Pip
if/else becomes in a builder block.Build whichever branch runs, and pass its result to buildEither(first:) for the first branch or buildEither(second:) for the second. Sugar and hand-written: Pip went home ! | Pip went home !
Pip went home . | Pip went home .
switch has three cases, and a builder has only buildEither(first:) and buildEither(second:). How is the middle case built?As a tree of two-way choices. "busy" is wrapped twice: buildEither(second:), then buildEither(first:) around that. Every case, traced: -- mood(0)
buildExpression("calm")
buildBlock([["calm"]])
buildEither(first: ["calm"])
buildEither(first: ["calm"])
buildBlock([["calm"]])
buildFinalResult(["calm"])
= calm
-- mood(1)
buildExpression("busy")
buildBlock([["busy"]])
buildEither(second: ["busy"])
buildEither(first: ["busy"])
buildBlock([["busy"]])
buildFinalResult(["busy"])
= busy
-- mood(2)
buildExpression("sleepy")
buildBlock([["sleepy"]])
buildEither(second: ["sleepy"])
buildBlock([["sleepy"]])
buildFinalResult(["sleepy"])
= sleepy
for … in loop becomes in a builder block.Build the loop body once per pass, keep each result, and when the loop is over pass all of them to buildArray at once. Sugar and hand-written: Pip went to Seattle to Victoria
Pip went to Seattle to Victoria
if #available with an else becomes in a builder block.The available branch's result goes through buildLimitedAvailability first, and then through buildEither like any branch. Sugar and hand-written, on iOS 27: Pip went today
Pip went today
A computed property; a closure parameter, where it applies to the closure the caller writes; and a protocol requirement, where conforming types inherit it — Dog.line has no attribute and is still built. They printed: Hello Pip
Pip sat
woof 2