SwiftUI ContentBuilder: one builder name for different content


Greetings, traveler!

SwiftUI now has a new ContentBuilder attribute. Its declaration is surprisingly small:

typealias ContentBuilder = ViewBuilder

So this is not a completely separate result builder. Apple defines ContentBuilder as a type alias for ViewBuilder.

But there is more behind this change than a new name.

ViewBuilder has always sounded like something specifically responsible for building views. Modern SwiftUI is broader than that. Declarative closures can describe views, toolbar items, commands, and other kinds of framework content.

With Xcode 27, SwiftUI starts moving these cases toward a shared, type-agnostic builder model.

Why the new name matters

SwiftUI has historically exposed several specialized result builders:

@ViewBuilder
@ToolbarContentBuilder
@CommandsBuilder
@TabContentBuilder
@KeyframeTrackContentBuilder
@CompositorContentBuilder

These names make the expected domain explicit, but they also expose implementation details at every declaration site.

A view closure needs a view builder. A toolbar closure needs a toolbar builder. Commands use another builder. Other SwiftUI domains may have their own variants.

With Xcode 27, Apple describes ContentBuilder as a unified replacement for type-specific builders such as ToolbarContentBuilder and CommandsBuilder.

That does not mean every specialized builder disappears from SwiftUI at once. Apple describes this as a step toward unified builders across SwiftUI.

But it introduces a broader vocabulary:

@ContentBuilder

Instead of saying what kind of content is being constructed through the builder name, the surrounding type can express that requirement.

Basic view example

For ordinary view content, ContentBuilder feels very familiar:

@ContentBuilder
private func header() -> some View {
    Text("Library")
        .font(.title)

    Text("Recently updated")
        .foregroundStyle(.secondary)
}

This is the same kind of declaration where we would traditionally use @ViewBuilder.

A custom container can use it as well:

struct Card<Content: View>: View {
    private let content: Content

    init(@ContentBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            content
        }
        .padding()
    }
}

Nothing about ContentBuilder changes the requirement of Card.

Content still has to conform to View:

struct Card<Content: View>: View

The builder constructs the content. The generic constraint determines what that content is allowed to be.

That distinction becomes more interesting outside normal view hierarchies.

Toolbar content uses the same builder

Consider toolbar content:

@ContentBuilder
private var editingActions: some ToolbarContent {
    ToolbarItem(placement: .primaryAction) {
        Button("Save") {
            save()
        }
    }

    ToolbarItem(placement: .secondaryAction) {
        Button("Cancel") {
            cancel()
        }
    }
}

The builder is still:

@ContentBuilder

but the resulting contract is:

some ToolbarContent

Compare that with the previous example:

@ContentBuilder
private var header: some View {
    Text("Profile")
}

Same builder, different content contract. This is the important part of the new model. The builder no longer needs to encode View or ToolbarContent in its name. The surrounding API already knows what kind of result it expects.

The change is deeper than the new name

Looking only at this:

typealias ContentBuilder = ViewBuilder

makes it easy to conclude that Apple simply introduced a more general name for an existing API.

The actual change goes deeper. When building with Xcode 27, SwiftUI’s builder machinery becomes type-agnostic. The builder itself no longer needs to require every expression inside the closure to conform to a particular SwiftUI content protocol.

This separates two responsibilities:

  1. Construct the structure produced by the closure.
  2. Determine whether that resulting structure is valid in the current context.

Previously, those concerns were more tightly coupled through specialized builders.

Now SwiftUI can use common construction machinery while letting the result type decide which protocol requirements must be satisfied.

This is why ContentBuilder makes sense as a name.

It builds content.

Whether that content ultimately represents View, ToolbarContent, Commands, or something else is a separate question.

Build the structure first, validate its capability afterward

Consider these two properties:

@ContentBuilder
private var profileContent: some View {
    Text("Account")
    Divider()
    Text("Settings")
}

and:

@ContentBuilder
private var profileToolbar: some ToolbarContent {
    ToolbarItem {
        Button("Edit") {
            startEditing()
        }
    }
}

Both declarations use the same builder.

Conceptually, ContentBuilder is responsible for taking multiple expressions and assembling them into a resulting content structure.

The context then provides the requirement.

For the first property:

some View

the produced content needs to be valid view content.

For the second:

some ToolbarContent

it needs to satisfy ToolbarContent instead.

A useful mental model is:

closure expressions
       
ContentBuilder
       
composed content
       
required protocol from context

This is different from thinking of the builder itself as the thing that determines the final domain.

The builder constructs the structure. The type system determines where that structure can be used.

What this does not change

ContentBuilder does not make different SwiftUI content types interchangeable.

For example:

@ContentBuilder
private var toolbar: some ToolbarContent {
    ToolbarItem {
        Button("Save") {
            save()
        }
    }
}

is valid because the result satisfies the ToolbarContent requirement.

That does not mean the same declaration suddenly becomes a normal view:

@ContentBuilder
private var content: some View {
    // ToolbarItem is not magically a View
}

Using a shared builder does not erase the differences between SwiftUI content protocols.

This is an important distinction because otherwise ContentBuilder might sound like some kind of type-erasing abstraction.

It is not.

SwiftUI remains strongly typed. The expected return type, generic constraint, or API parameter still determines which content is valid.

Conditional conformances preserve type safety

If ContentBuilder itself no longer imposes a specific content protocol, something else has to preserve those rules.

This is where conditional conformances become important.

Imagine a simplified generic container:

struct ContentPair<First, Second> {
    let first: First
    let second: Second
}

The type itself does not initially say anything about SwiftUI.

But we can make it a View only when both values are also views:

extension ContentPair: View where First: View, Second: View {
    var body: some View {
        VStack {
            first
            second
        }
    }
}

The generic structure is reusable, while its capabilities depend on the types it contains.

SwiftUI applies the same general idea to the content types produced by its builders.

Apple specifically points to TupleContent as an example. It can conditionally conform to SwiftUI content protocols depending on the types of the elements it contains.

Conceptually:

ComposedContent<A, B>
        │
        ├── can be View
        │   when A and B satisfy View requirements
        │
        └── can participate in another content domain
            when its elements satisfy that domain

This allows the builder itself to remain type-agnostic without giving up compile-time safety.

Why specialized builders made type checking harder

This architecture also explains why ContentBuilder is more than API cleanup.

SwiftUI code gives the compiler a difficult job. Result builders, generic types, overloaded initializers, conditional content, opaque return types, and nested containers can all participate in the same expression.

Before unified builders, a primitive that supported several kinds of SwiftUI content could require several builder-specific entry points.

A simplified API might conceptually look like this:

func compose<Content>(
    @ViewBuilder content: () -> Content
) where Content: View

and another version for a different domain:

func compose<Content>(
    @ToolbarContentBuilder content: () -> Content
) where Content: ToolbarContent

At the call site, however, both might simply look like:

compose {
    // content
}

The builder name is not visible there. The compiler therefore needs to use the closure body and its surrounding context to determine which candidate applies.

Now put another overloaded container inside that closure, then another one inside it. The compiler has more possible choices to consider while solving the expression.

This is especially relevant to shared SwiftUI primitives such as Group, Section, and ForEach, which can appear deeply nested in declarative code.

One construction path means less work for the compiler

With the unified builder model, SwiftUI can reduce those builder-specific choices.

Instead of providing separate construction paths purely because the resulting content belongs to different domains, the framework can use common builder machinery and express the distinction through the resulting type.

Conceptually, the important part moves from:

Which builder-specific overload should construct this closure?

toward:

Does the resulting content satisfy the requirement of this API?

Apple explicitly says that one of the reasons for unifying result builders under ContentBuilder is to substantially improve SwiftUI type-checking performance.

This does not mean every slow SwiftUI expression suddenly becomes cheap to compile. SwiftUI still makes heavy use of generics and type inference.

But it removes one important source of unnecessary work: multiple builder-specific entry points that can otherwise look indistinguishable at the call site.

So ContentBuilder is not a runtime performance feature. It will not make view updates, layout, rendering, or animations faster. The improvement is on the compiler side.

A small source compatibility trade-off

Moving protocol requirements out of builder blocks changes some assumptions that existing code may have relied on.

Apple notes that most SwiftUI projects continue to compile without modification, but there are some source compatibility cases.

One example is code that explicitly depends on old concrete builder result types such as TupleView.

With the unified builder model, multi-expression content can now use TupleContent instead.

In most application code, this distinction is invisible because we normally write:

some View

rather than depending on the concrete type produced by a result builder.

That remains the preferable approach. It is another good reason not to couple application APIs to SwiftUI’s generated concrete builder types unless there is a strong reason to do so.

Availability and migration

There are two different kinds of availability worth separating here.

First, deployment target. ContentBuilder can be used while targeting older versions of Apple’s operating systems. You do not need to raise your minimum deployment target to iOS 27 just to adopt it.

The unified type-agnostic behavior arrives when building with Xcode 27.

This means a project can, for example, continue supporting an older iOS version while benefiting from the new builder model when compiled with Xcode 27.

There is also no reason to immediately replace every existing:

@ViewBuilder

with:

@ContentBuilder

Apple explicitly keeps ViewBuilder, and in Xcode 27 its closures also participate in the new type-agnostic behavior.

Existing code can remain unchanged. For new APIs, however, ContentBuilder can communicate intent more accurately when the abstraction is about SwiftUI content rather than specifically about views.

For example:

init(@ContentBuilder content: () -> Content)

is a good fit for a generic SwiftUI content-building API.

If an existing API already uses:

init(@ViewBuilder content: () -> Content)

and only ever accepts View content, changing it purely for consistency provides little practical benefit.

When should you use ContentBuilder?

For new code built with Xcode 27, I would consider ContentBuilder when designing APIs around SwiftUI’s declarative content model.

It is especially appropriate when the builder itself does not need to communicate a view-specific restriction.

For example:

struct Panel<Content: View>: View {
    let content: Content

    init(@ContentBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        content
            .padding()
    }
}

The constraint:

Content: View

already explains what the resulting content must be. @ContentBuilder only explains how the closure is assembled. That separation is consistent with the direction SwiftUI is taking.

At the same time, there is no migration race here. Existing @ViewBuilder APIs are not suddenly wrong or deprecated just because ContentBuilder exists.

Final thought

ContentBuilder looks tiny when you first encounter it:

typealias ContentBuilder = ViewBuilder

But treating it as only a rename misses the more interesting change. SwiftUI is separating the mechanism that constructs declarative content from the protocol that defines what that content can do.

The builder can be shared. The surrounding type keeps the rules strict. That gives SwiftUI a path toward fewer specialized builder entry points, a more consistent API vocabulary, and less work for the compiler when resolving complex declarative expressions.

So the interesting part of ContentBuilder is not that ViewBuilder received a broader name. It is that SwiftUI’s builder model itself became broader.