Erez Hod

Understanding @ViewBuilder in SwiftUI: Dynamic and Static View Construction in Custom Views

SwiftUI is a game-changer for building user interfaces across Apple platforms. One of the most powerful tools it offers is @ViewBuilder, a specialized result builder that transforms closures into SwiftUI views. If you’ve been working with SwiftUI for a while, you’ve likely encountered @ViewBuilder, but you might still be wondering: What exactly does it do? And more importantly, how should it be used in different scenarios?

In this post, we’ll explore @ViewBuilder from the ground up, dissect its implementation, and dive into two key methodologies for using it in SwiftUI views—embedding it as a property vs. using it in the initializer. Along the way, we’ll unpack the differences between these approaches and help you choose the right one for your SwiftUI apps.

What is @ViewBuilder?

At its core, @ViewBuilder is a result builder—a Swift feature that enables functions or closures to return multiple views in a declarative way. Introduced with SwiftUI, @ViewBuilder allows us to group multiple View objects together in a closure without needing to explicitly return them as a Tuple or Array.

Normally, Swift functions return a single value, but SwiftUI views are often composed of multiple subviews. That’s where @ViewBuilder comes in—it enables SwiftUI to “build” complex hierarchies of views from within a single closure.

Here’s a simple example of @ViewBuilder in action:

@ViewBuilder
func buildWelcomeView() -> some View {
    Text("Hello, Mr. Skywalker!")
    Button("Click me") {
        print("Button clicked")
    }
}

Without @ViewBuilder, you’d need to return these views in a tuple or encapsulate them in a container like a VStack or HStack. But @ViewBuilder allows Swift to treat these multiple views as a cohesive group, simplifying how we construct UI components.

@ViewBuilder in SwiftUI Custom Views

Using @ViewBuilder becomes particularly useful when creating custom SwiftUI components that require flexible, user-provided content. You can declare content as a ViewBuilder-enabled closure, giving developers the freedom to inject various view hierarchies into your component.

But what’s the best way to integrate @ViewBuilder into your custom SwiftUI views? There are two common methodologies, each with its own nuances: using @ViewBuilder as a property or embedding it within the initializer.

Approach #1: @ViewBuilder as a Property

In this first approach, we apply @ViewBuilder directly to a property:

struct ContainerOneView<Content: View>: View {
    @ViewBuilder var content: () -> Content

    var body: some View {
        VStack {
            content()
        }
    }
}

Here, the content is passed as a closure, which is evaluated whenever the body of the view is recomputed. This is a dynamic approach—every time SwiftUI needs to re-render the view (due to state changes or other triggers), it will call content() again, building a fresh set of views.

🙋‍♀️ Why Use This Approach?

This methodology is highly flexible and dynamic. The content closure gets evaluated every time the view is updated, meaning it can adapt to changes in state or environment. It allows your custom view to change its child views dynamically, which can be useful in reactive UI updates.

For example, if your ContainerOneView is inside a View that responds to user input, SwiftUI will rebuild the content every time the state changes, ensuring that the UI remains in sync with the data.

Approach #2: @ViewBuilder in the Initializer

In contrast, the second approach moves @ViewBuilder to the initializer, where the content is evaluated once and stored as a value:

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

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

    var body: some View {
        VStack {
            content
        }
    }
}

This design stores the result of the @ViewBuilder closure in a property during initialization. As a result, the content is fixed once the view is created, and no further evaluation of the closure occurs during subsequent body re-computations.

🙋 Why Use This Approach?

This approach is ideal when your view’s content is static—meaning it doesn’t change dynamically during runtime. The performance benefit comes from not needing to re-evaluate the closure on each update cycle. If the content remains constant throughout the lifecycle of the view, this method can reduce unnecessary overhead and make your app run more efficiently.

Key Differences Between the Two Approaches

Both approaches leverage @ViewBuilder, but they differ in when and how often the content is evaluated.

  1. Reactivity

    1. Dynamic (Property): In the first approach, the closure is evaluated each time the body is computed. This means the content can change based on runtime conditions, making it suitable for dynamic views.

    2. Static (Initializer): In the second approach, the content is evaluated once during initialization. The content is then stored as a constant and does not change unless the view is re-initialized.

  2. Use Case

    1. Dynamic Content: If the content needs to update frequently, such as when responding to state changes, the property approach is better.

    2. Static Content: When the content remains constant and doesn’t need to be dynamically rebuilt, using the initializer is more efficient.

  3. Performance

    1. Dynamic (Property): The overhead of re-evaluating the closure on each view update can be higher, but this is necessary when dealing with dynamic content.

    2. Static (Initializer): This approach can improve performance by storing a pre-built view, which is faster for views that don’t need to change.

Conclusion

Understanding the subtle differences between using @ViewBuilder as a property versus using it in an initializer can significantly impact how your SwiftUI app behaves, both in terms of flexibility and performance.

When your content is dynamic and needs to react to state changes, applying @ViewBuilder as a property gives you the power to rebuild the view as needed. However, when your content is static and doesn’t need frequent updates, embedding @ViewBuilder in the initializer offers a more efficient approach.

Mastering this balance will help you craft better-performing, more responsive SwiftUI apps that take full advantage of Swift’s declarative UI framework.

👨🏻‍💻 Happy coding!

Get Weekly Insights

Receive the latest articles, tips, and updates on development for Apple platforms right in your inbox.