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

  1. 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.


  1. 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 state

Why 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! 🧱