SwiftUI’s declarative syntax often feels like magic. You can write natural-looking code with if statements, multiple view expressions, even loops — and somehow, it all just works. But how?
The secret lies in ViewBuilder — one of Swift’s most powerful tools that transforms your declarative code into optimized SwiftUI types. Let’s dive into what makes this possible.
The Foundation: Result Builders
Before focusing on ViewBuilder itself, it’s important to understand what it’s built on: Swift’s Result Builder system (previously called Function Builders).
Result builders are a compiler feature that lets you build domain-specific languages (DSLs) by transforming blocks of code into other structures — in SwiftUI’s case, views.
The Problem Without ViewBuilder
Imagine SwiftUI didn’t have ViewBuilder. Here’s what you’d be dealing with:
// ❌ This wouldn't compile without ViewBuilder
struct MyView: View {
let showDetails: Bool
var body: some View {
if showDetails {
DetailView() // Type: DetailView
} else {
SummaryView() // Type: SummaryView
}
// Error: inconsistent return types!
}
}What’s the issue?
Swift’s type system expects a consistent return type — but DetailView and SummaryView are two different types.
Without ViewBuilder, we’d need clunky workarounds like type erasure:
// ❌ Ugly workaround
var body: some View {
if showDetails {
return AnyView(DetailView()) // Type-erased
} else {
return AnyView(SummaryView()) // Adds runtime cost
}
}This works, but at the cost of performance and type safety.
The Magic of ViewBuilder
With ViewBuilder, SwiftUI handles the transformation behind the scenes:
// ✅ What you write
@ViewBuilder
var body: some View {
if showDetails {
DetailView()
} else {
SummaryView()
}
}This gets compiled roughly into:
// 🔄 What the compiler generates
var body: some View {
let result: _ConditionalContent<DetailView, SummaryView>
if showDetails {
result = ViewBuilder.buildEither(first: DetailView())
} else {
result = ViewBuilder.buildEither(second: SummaryView())
}
return ViewBuilder.buildBlock(result)
}Pretty cool, right? Let’s see how this works in more situations.
How ViewBuilder Transforms Code
ViewBuilder uses different transformation methods depending on the structure of your code. Here’s a breakdown:
1. Conditions: buildEither
When using if/else, ViewBuilder transforms it like this:
// Original
if isLoggedIn {
ProfileView()
} else {
LoginView()
}
// Transformed
ViewBuilder.buildEither(first: ProfileView()) // or second: LoginView()➡️ Both branches return a _ConditionalContent type, which maintains type consistency while preserving concrete types for performance.
2. Optional Views: buildOptional
For if statements without else:
// Original
if shouldShow {
NotificationView()
}
// Transformed
ViewBuilder.buildOptional(shouldShow ? NotificationView() : nil)➡️ The result is an optional view — great for views that appear conditionally.
3. Multiple Views: buildBlock
For multiple view expressions:
// Original
Text("Title")
Image(systemName: "star")
Button("Action") { }
// Transformed
ViewBuilder.buildBlock(
Text("Title"),
Image(systemName: "star"),
Button("Action") { }
)➡️ The result is a TupleView, which explains SwiftUI’s 10-view limit per container — it’s tied to buildBlock overloads.
4. Combining Constructs
These transformations can also be mixed:
VStack {
Text("Header")
if isLoading {
ProgressView()
} else {
ContentView()
}
Text("Footer")
}The compiler does something like:
let conditional = isLoading
? ViewBuilder.buildEither(first: ProgressView())
: ViewBuilder.buildEither(second: ContentView())
ViewBuilder.buildBlock(
Text("Header"),
conditional,
Text("Footer")
)➡️ Resulting in a TupleView<(Text, _ConditionalContent, Text)>.
The Loop Exception: Why ForEach Is Special
Many devs wonder why they can’t use a for loop inside a ViewBuilder:
// ❌ Won’t compile
@ViewBuilder
var content: some View {
for item in items {
Text(item.name)
}
}Instead, SwiftUI provides:
// ✅ Works
ForEach(items, id: \.self) { item in
Text(item.name)
}Why Not buildArray?
It may seem logical for SwiftUI to support buildArray like other builders — but there are deep reasons why it doesn’t.
Let’s imagine what it might look like:
extension ViewBuilder {
static func buildArray<Element: View>(_ elements: [Element]) -> some View {
VStack {
ForEach(elements.indices, id: \.self) { index in
elements[index]
}
}
}
}So why doesn’t SwiftUI use this?
The Core Problems with buildArray
- Immediate Evaluation
// All views are created up front
@ViewBuilder
var content: some View {
for user in users {
UserRowView(user: user)
}
}This would be transformed into:
ViewBuilder.buildArray(
users.map { UserRowView(user: $0) }
)➡️ All views are evaluated immediately, which breaks SwiftUI’s lazy rendering.
- No Identity Tracking
let views = users.map { UserRowView(user: $0) }
// SwiftUI can’t tell which view maps to which user.
// Result: No smooth animations, poor diffing, lost stateWhy ForEach Solves It
Apple solved these problems by designing ForEach as a specialized view that integrates with SwiftUI’s rendering engine:
struct ForEach<Data, ID, Content>: View
where Data: RandomAccessCollection, ID: Hashable, Content: View {
let data: Data
let id: KeyPath<Data.Element, ID>
let content: (Data.Element) -> Content
}Internally, it uses lazy rendering, identity tracking, diffing, and recycling to ensure performance and correctness — something a simple array-based builder couldn’t handle.
Conclusion
What feels like SwiftUI “magic” is actually a well-crafted system built on ResultBuilder transformations. ViewBuilder handles the complexity of conditions, optionals, and multiple views — while ForEach takes care of performant rendering for collections.
Understanding how it all works gives you superpowers when debugging and building more scalable UI structures.
Happy building! 🧱