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