Skip to content
ExponaBLOG
SWIFT, SWIFTUI & IOS APPS · DAY 085 / 100

Result builders: Swift & iOS best practices explained

How SwiftUI's syntax works.

Expona Admin8 min read

Introduction

Result builders are one of Swift's most powerful yet underutilized features, and they're the backbone of why SwiftUI feels so natural to write. Understanding result builders: Swift & iOS best practices explained transforms how you'll approach domain-specific languages (DSLs) and view hierarchies in your apps. In this guide, we'll walk through what result builders actually do, how ViewBuilder powers SwiftUI, and how to build your own for maximum flexibility.

What @resultBuilder Actually Does

A result builder is a type that transforms a series of statements into a single value. Rather than forcing developers to manually construct nested data structures, result builders let us write clean, sequential code that gets automatically converted into the desired structure. Think of it as a compiler-level transformation that intercepts your code block and rebuilds it into something else.

When you mark a function parameter or closure with @resultBuilder, you're telling Swift: "Whatever code the developer writes in this block, pass it to my result builder type so I can transform it." The result builder then implements static methods like buildBlock, buildIf, and buildArray to handle different syntactic patterns.

Without result builders, creating a complex view hierarchy in SwiftUI would look like this:

swift
VStack(
    alignment: .leading,
    spacing: 16,
    content: {
        Text("Hello")
        Spacer()
        Button("Tap me") { }
    }
)

With @ViewBuilder (a result builder), we write the natural, clean version:

swift
VStack(alignment: .leading, spacing: 16) {
    Text("Hello")
    Spacer()
    Button("Tap me") { }
}

The difference isn't just syntax sugar—it's about readability, maintainability, and developer experience. Result builders eliminate the cognitive overhead of mentally nesting closures.

ViewBuilder: Building SwiftUI View Hierarchies

SwiftUI's @ViewBuilder is the canonical example of a result builder in production. Every SwiftUI container—VStack, HStack, ZStack, Group, ForEach—uses it. Understanding ViewBuilder helps us grasp why certain SwiftUI patterns work the way they do.

ViewBuilder implements methods like:

  • buildBlock: handles sequential views
  • buildIf: handles optional views
  • buildEither: handles conditional branches
  • buildArray: handles loops and dynamic content

Here's a practical scenario: suppose we're building a user profile card that conditionally shows different sections based on user state.

swift
struct ProfileCard<Content: View>: View {
    let user: User
    @ViewBuilder var content: () -> Content
    
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack {
                Image(systemName: "person.circle")
                VStack(alignment: .leading) {
                    Text(user.name)
                        .font(.headline)
                    Text(user.email)
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
            }
            
            Divider()
            
            content()
        }
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(12)
    }
}

// Usage
ProfileCard(user: currentUser) {
    if user.isPremium {
        Label("Premium Member", systemImage: "star.fill")
    }
    if let location = user.location {
        Label(location, systemImage: "mappin")
    }
    Button(action: { /* edit */ }) {
        Text("Edit Profile")
    }
}

Notice how we can write multiple views sequentially inside the closure, and even use if statements directly. Without ViewBuilder, we'd need to manually wrap these in tuples or arrays. ViewBuilder's buildIf method handles the conditional logic, and buildBlock chains the views together.

The magic happens at compile time. When Swift sees your sequential statements inside a @ViewBuilder block, it transforms them into a single view type. This is why SwiftUI views can have up to 10 direct children in a container—ViewBuilder chains them into a nested generic type that satisfies the View protocol.

Writing Your Own Result Builders for DSLs

Result builders aren't limited to SwiftUI. They're perfect for building domain-specific languages within Swift. Let's build a practical example: a fluent API for constructing HTML or structured data.

First, define your result builder type:

swift
@resultBuilder
struct FormBuilder {
    static func buildBlock(_ components: FormField...) -> [FormField] {
        components
    }
    
    static func buildIf(_ component: [FormField]?) -> [FormField] {
        component ?? []
    }
    
    static func buildEither(first: [FormField]) -> [FormField] {
        first
    }
    
    static func buildEither(second: [FormField]) -> [FormField] {
        second
    }
}

struct FormField {
    let label: String
    let type: FieldType
    let isRequired: Bool
}

enum FieldType {
    case text
    case email
    case password
    case checkbox
}

struct Form {
    let title: String
    @FormBuilder var fields: [FormField]
    
    init(
        title: String,
        @FormBuilder _ fieldBuilder: () -> [FormField]
    ) {
        self.title = title
        self.fields = fieldBuilder()
    }
}

Now we can build forms with a clean, declarative syntax:

swift
let registrationForm = Form(title: "Sign Up") {
    FormField(label: "Email", type: .email, isRequired: true)
    FormField(label: "Password", type: .password, isRequired: true)
    FormField(label: "Confirm Password", type: .password, isRequired: true)
    
    if includeNewsletterSignup {
        FormField(label: "Subscribe to newsletter", type: .checkbox, isRequired: false)
    }
}

This is far cleaner than manually constructing an array. Your team members can add fields intuitively without thinking about array syntax or closures. The result builder handles the transformation from sequential statements into a structured array.

For a more complex example, consider building a state machine DSL:

swift
@resultBuilder
struct StateBuilder {
    static func buildBlock(_ components: State...) -> [State] {
        components
    }
}

struct State {
    let name: String
    let onEnter: (() -> Void)?
    let transitions: [Transition]
}

struct Transition {
    let event: String
    let targetState: String
}

struct StateMachine {
    @StateBuilder var states: [State]
}

Result builders eliminate boilerplate and make your DSL feel native to Swift. Developers using your API can read it like prose rather than code.

Supporting if and for in Blocks

One of result builders' most powerful features is transparent support for control flow. You don't need to do anything special—implement the right methods and Swift handles it automatically.

For if statements, implement buildIf and buildEither:

swift
@resultBuilder
struct ArrayBuilder<T> {
    static func buildBlock(_ components: T...) -> [T] {
        components
    }
    
    // Handles if without else (optional)
    static func buildIf(_ component: [T]?) -> [T] {
        component ?? []
    }
    
    // Handles if-else branching
    static func buildEither(first: [T]) -> [T] {
        first
    }
    
    static func buildEither(second: [T]) -> [T] {
        second
    }
    
    // Handles for-in loops
    static func buildArray(_ components: [[T]]) -> [T] {
        components.flatMap { $0 }
    }
}

With these methods, developers can write intuitive control flow:

swift
struct DataList {
    let items: [String]
    
    @ArrayBuilder<String>
    var processedItems: [String] {
        "Header"
        
        if items.count > 0 {
            "Has items:"
            for item in items {
                item.uppercased()
            }
        } else {
            "Empty list"
        }
        
        "Footer"
    }
}

When this code compiles, Swift transforms it into a single array through a series of buildBlock, buildIf, buildEither, and buildArray calls. The developer writes imperative-looking code, but the result builder constructs the declarative data structure.

This is particularly powerful in SwiftUI when dealing with dynamic lists:

swift
struct ContentView: View {
    let numbers = [1, 2, 3, 4, 5]
    
    var body: some View {
        VStack {
            Text("Numbers:")
            
            if !numbers.isEmpty {
                ForEach(numbers, id: \.self) { number in
                    Text("\(number)")
                }
            } else {
                Text("No numbers")
            }
        }
    }
}

ViewBuilder's buildArray method handles the ForEach internally, allowing loops to work seamlessly alongside regular views.

Common Mistakes

Mistake 1: Forgetting to implement buildIf for optional behavior. If your result builder needs to support conditional views but you only implement buildBlock, the compiler will error. Always include both buildIf and buildEither if you expect developers to use conditionals.

Mistake 2: Returning the wrong type from buildBlock. A common error is returning a single component instead of an array when you need to combine multiple elements. If your builder chains five statements, buildBlock receives all five as separate arguments and must combine them appropriately.

Mistake 3: Misunderstanding buildArray scope. The buildArray method receives an array of already-transformed components. If you're building a DSL where loops should produce multiple final items, ensure your loop transformation happens before buildArray receives it, or implement buildArray to handle the nested structure correctly.

Mistake 4: Overcomplicating the type system. Result builders work best when the result type is simple and consistent. If you're trying to build a heterogeneous collection (mixing types), you'll fight the type checker. Keep your builder's output type focused.

Key Takeaways

  • Result builders transform sequential statements into structured values at compile time, eliminating nested closure boilerplate.
  • ViewBuilder powers SwiftUI's clean syntax by implementing buildBlock, buildIf, buildEither, and buildArray methods.
  • Build your own result builders for domain-specific languages—form builders, configuration systems, or state machines—to create intuitive APIs that feel native to Swift.
  • Implement buildIf and both buildEither overloads if you want developers to use if statements; implement buildArray to support loops.
  • Result builders handle control flow transparently; you don't need special syntax, just the right static methods.
  • Test result builder edge cases thoroughly: empty blocks, deeply nested conditionals, and mixed control flow patterns often reveal design issues early.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles

Result builders | Swift, SwiftUI & iOS Apps | Expona