Swift and SwiftUI tutorials for Swift Developers

@ViewBuilder vs struct View in SwiftUI

In the current Swift programming ecosystem, the arrival of SwiftUI marked a revolution. We moved from the imperative construction of UIKit to a declarative paradigm that allows us to describe complex interfaces with amazing speed. However, as Xcode projects grow, every iOS developer sooner or later faces a common enemy: the “Massive View”.

You start with a simple VStack. You add an image, then some text, then a button, then conditional logic for the loading state… and suddenly, your body property has 300 lines of nested code, difficult to read, impossible to test, and painful to maintain.

To solve this, SwiftUI offers us two main paths to decompose and organize our UI:

  1. Extract sub-views into new structures (struct View).
  2. Use properties or functions marked with @ViewBuilder.

But which is better? Are they interchangeable? When should you use a full structure and when a simple computed variable? In this tutorial, we will break down the battle of @ViewBuilder vs struct View in SwiftUI, analyzing their differences, similarities, and performance impact for your applications on iOS, macOS, and watchOS.


1. The Heavyweight Contender: struct View

In SwiftUI, a view is, by definition, a structure (struct) that conforms to the View protocol. When you create a new file in Xcode, this is what you get by default. It is the “canonical” and most robust way to encapsulate interface and behavior.

What is it really?

When creating a separate struct, you are defining a new type in the Swift system. This has profound implications. Being a Value Type, it is immutable and lightweight, but being a differentiated entity, the SwiftUI system can track it independently within the view dependency graph.

// struct View Example
struct UserProfileHeader: View {
    let username: String
    
    var body: some View {
        HStack {
            Image(systemName: "person.circle")
            Text(username)
                .font(.headline)
        }
        .padding()
    }
}

2. The Agile Contender: @ViewBuilder

@ViewBuilder is technically a Result Builder, a powerful feature introduced in Swift. It is the attribute that allows containers like VStack, HStack, or Group to accept declarative code blocks without needing to explicitly return an array of views or use the return keyword.

Many developers use let, var, or func decorated with @ViewBuilder to split a giant body into smaller chunks within the same file.

What is it really?

It is not a view itself, but a mechanism to build views. When you extract code to a helper variable or function inside your main view, you are not creating a new type. You are simply organizing the code. To the compiler, it is as if you had copied and pasted that code directly into the body.

// Example of a property with @ViewBuilder inside a parent view
@ViewBuilder
var headerView: some View {
    if isActive {
        Text("Active User")
    } else {
        Text("Inactive")
            .foregroundColor(.gray)
    }
}

3. Key Differences: Performance, State, and Reuse

Here is where the expert iOS developer distinguishes themselves from the beginner. The choice between @ViewBuilder vs struct View in SwiftUI is not just a matter of style; it directly affects the architecture and fluidity of your app.

A. The Redraw Cycle

This is the most critical difference for optimization in SwiftUI.

  • The Struct View Case:
    SwiftUI is smart. When you use a separate struct View, the system understands that it is an isolated component. If the parent view (ParentView) updates (redraws) because a state changed that does not affect the parameters passed to the child view (ChildView), SwiftUI can choose not to redraw or recalculate the child’s body. It acts as a natural performance barrier.
  • The @ViewBuilder Case:
    A property or function is an intrinsic part of the container struct. If the parent view redraws for any reason (even a change in a variable that your helper function doesn’t use), the body of your @ViewBuilder function will be re-evaluated. There is no isolation. If you have complex calculations or expensive shadows in that function, they will execute again and again unnecessarily.

B. State Management (@State)

  • Struct View: Can have its own state storage. You can declare @State, @StateObject, or @FocusState properties inside it. This is vital for reusable components like a custom text field or a button with internal animation.
  • @ViewBuilder: Cannot possess its own state. It shares and depends entirely on the container view’s state.

C. Dependency Injection

  • Struct View: Requires explicit injection. You must pass the data it needs through its initializer (let user: User). This makes the data flow clear and predictable (“One way data flow”).
  • @ViewBuilder: Has implicit access to all properties (let, var, @Environment) of the parent view. This is convenient (less “boilerplate” code), but strongly couples the code fragment to the parent view.

4. Practical Tutorial: Refactoring in Xcode

Let’s visualize this with a real Swift programming example. Imagine a “Product Detail” view.

Step 1: The Monolith

struct ProductDetailView: View {
    let product: Product
    @State private var isZoomed = false
    
    var body: some View {
        ScrollView {
            VStack {
                // Image Section (Complex)
                ZStack {
                    Image(product.imageName)
                        .resizable()
                        .aspectRatio(contentMode: isZoomed ? .fill : .fit)
                        .onTapGesture { isZoomed.toggle() }
                    
                    if !isZoomed {
                        Image(systemName: "magnifyingglass")
                    }
                }
                
                // Information Section
                Text(product.title).font(.largeTitle)
                Text(product.description).foregroundColor(.secondary)
                
                // Buy Button
                Button("Buy Now") { ... }
                    .padding()
                    .background(Color.blue)
            }
        }
    }
}

Step 2: Cleanup with @ViewBuilder

If we just want to organize the code to read it better, we use @ViewBuilder.

extension ProductDetailView {
    @ViewBuilder
    var imageSection: some View {
        ZStack {
            Image(product.imageName) // Direct access to parent's 'product'
                .resizable()
                .aspectRatio(contentMode: isZoomed ? .fill : .fit) // Direct access to state
                .onTapGesture { isZoomed.toggle() }
            
            if !isZoomed {
                Image(systemName: "magnifyingglass")
            }
        }
    }
}

Verdict: It is fast and clean, but if something changes in ProductDetailView that has nothing to do with the image, imageSection will be recalculated.

Step 3: Optimization with struct View

If the image is complex or we want to reuse this image viewer elsewhere in the app, we create a struct.

struct ProductImageView: View {
    let imageName: String // Explicit dependency
    @State private var isZoomed = false // Isolated/Internal state
    
    var body: some View {
        ZStack {
            Image(imageName)
                .resizable()
                .aspectRatio(contentMode: isZoomed ? .fill : .fit)
                .onTapGesture { isZoomed.toggle() }
            
            if !isZoomed {
                Image(systemName: "magnifyingglass")
            }
        }
    }
}

Verdict: Now ProductImageView is an independent component. Xcode can preview it (Preview) separately, and it is much more efficient in terms of performance.


5. The Decision Matrix: When to use what?

To optimize your development in SwiftUI and Swift, print this list and keep it near your monitor:

Use @ViewBuilder variables or functions when:

  1. Visual Organization: You only want to split a long body into logical sections (Header, Content, Footer) to improve readability.
  2. Simple Logic: You need to conditionally render (if/else) small parts of the UI.
  3. High Coupling: The view fragment needs to access many @Environment or @State variables from the parent, and passing them all as parameters to a struct would be excessively tedious.
  4. No Own State: The fragment does not need to manage its own interaction or internal animation.

Use struct View when:

  1. Reuse: The component will be used in more than one place in the app (e.g., a custom list cell, a styled button).
  2. Independent State: The component needs its own @State (e.g., an input form, a counter).
  3. Performance Optimization: You want to isolate redrawing. If the parent changes frequently, the child should not be affected.
  4. Complexity: The component is complex enough to deserve its own file and tests.
  5. Xcode Previews: You want to be able to work on the design of that component in isolation on the Canvas.

6. Important Similarities

Despite their differences in internal architecture, it is worth remembering what they have in common so you don’t fear using both:

  • Composition: Both are first-class citizens in SwiftUI composition. You can put a struct View inside a @ViewBuilder and vice versa.
  • Typing: Both return some View. Swift hides the complexity of the concrete type (which is usually monstrous like TupleView<(Text, Image)>), allowing you to work with clean abstractions.
  • Syntax: Thanks to improvements in Swift, the visual syntax is almost identical inside the body.

Conclusion

In modern Swift programming, there is no absolute winner in the battle @ViewBuilder vs struct View in SwiftUI. Both are essential tools.

The rookie mistake is using @ViewBuilder for everything out of laziness to create new files. The purist’s mistake is creating a struct for every text label, filling the project with hundreds of unnecessary micro-files.

As an iOS developer, your goal should be balance:

  1. Start prototyping fast in the body.
  2. Use @ViewBuilder to organize that body into readable sections (header, content, footer).
  3. Refactor to struct View as soon as you detect: Reuse, State Complexity, or Performance Issues.

Mastering this distinction is what will allow you to build applications in Xcode that not only look good on iOS, macOS, and watchOS, but run with perfect fluidity at 60 (or 120) frames per second.

If you have any questions about this article, please contact me and I will be happy to help you 🙂. You can contact me on my X profile or on my Instagram profile.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

@ViewBuilder vs ViewModifier in SwiftUI

Next Article

@Binding vs @Bindable in SwiftUI

Related Posts