Custom View Modifiers & ViewBuilder

Reviewed & published by Brayan K

As your app grows you'll repeat the same styling and structure. SwiftUI lets you package reusable styles in a ViewModifier, expose them cleanly with View extensions, accept flexible content with @ViewBuilder, and extract repeated UI into reusable views. This is how you keep a large codebase DRY.

Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

Building iOS Apps with SwiftUI • Intermediate

What You'll Learn in This Lesson

1️⃣ Custom ViewModifier

Conform a type to ViewModifier and implement body(content:). The content parameter is the view you're modifying; return it wrapped with your changes. Apply it with .modifier(...) to reuse a consistent style everywhere.

import SwiftUI

// A custom ViewModifier packages a reusable set of changes. Implement
// body(content:) and return 'content' wrapped with your modifications.
struct CardStyle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(Color(.systemBackground))
            .cornerRadius(12)
            .shadow(radius: 4)
    }
}

// Apply it with .modifier(...). Now any view gets the same card look.
struct CardDemo: View {
    var body: some View {
        Text("Reusable card")
            .modifier(CardStyle())
    }
}

2️⃣ A Clean API with a View Extension

Calling .modifier(CardStyle()) works but reads awkwardly. Add an extension on View with a convenience method so you can write .cardStyle() — exactly like a built-in modifier.

import SwiftUI

// A View extension makes the modifier read like a built-in one.
extension View {
    func cardStyle() -> some View {
        modifier(CardStyle())
    }
}

// Much nicer to call: .cardStyle() instead of .modifier(CardStyle()).
struct NiceCard: View {
    var body: some View {
        VStack {
            Text("Title").font(.headline)
            Text("Body text")
        }
        .cardStyle()
    }
}

3️⃣ @ViewBuilder — Accepting Multiple Children

@ViewBuilder lets a closure return several views (and use if/switch). It's why a VStack's trailing closure can hold many children. Mark your own content closures with it to build flexible container views.

import SwiftUI

// @ViewBuilder lets a closure return MULTIPLE views and use if/switch.
// That's why a VStack's trailing closure can hold several children.
struct LabeledBox<Content: View>: View {
    let title: String
    // the content closure is a view builder, so callers pass many views
    @ViewBuilder let content: () -> Content

    var body: some View {
        VStack(alignment: .leading) {
            Text(title).font(.caption).foregroundColor(.secondary)
            content()        // drop the caller's views in here
        }
        .padding()
    }
}

struct UseLabeledBox: View {
    var body: some View {
        LabeledBox(title: "Profile") {
            Text("Ada Lovelace")     // multiple child views, thanks to
            Text("iOS Developer")    // @ViewBuilder on 'content'
        }
    }
}

4️⃣ Extracting Reusable Views

When you repeat a chunk of UI, pull it into its own View struct that takes parameters. This keeps your code DRY, your body readable, and your styling consistent — and it pairs naturally with custom modifiers.

import SwiftUI

// Extract repeated UI into its own View struct to stay DRY and readable.
struct StatTile: View {
    let label: String
    let value: String

    var body: some View {
        VStack {
            Text(value).font(.title).bold()
            Text(label).font(.caption)
        }
        .cardStyle()
    }
}

struct Dashboard: View {
    var body: some View {
        HStack {
            StatTile(label: "Steps", value: "8,420")
            StatTile(label: "Calories", value: "612")
        }
    }
}

Your turn. Fill in the blanks to build a reusable pill style and expose it as .pillStyle().

// 🎯 YOUR TURN — fill in each ___ then preview in Xcode.
import SwiftUI

// 1) Make a reusable "pill" style as a ViewModifier.
struct PillStyle: ___ {              // 👉 the modifier protocol
    func body(content: Content) -> some View {
        content
            .padding(.horizontal, 12)
            .padding(.vertical, 6)
            .background(Color.blue.opacity(0.2))
            .cornerRadius(20)
    }
}

// 2) Add a convenience method on View so you can call .pillStyle().
extension View {
    func pillStyle() -> some View {
        ___(PillStyle())             // 👉 the method that applies a modifier
    }
}

struct Tag: View {
    var body: some View {
        Text("SwiftUI").pillStyle()
    }
}

Pro Tips

Common Errors (and the fix)

Mini-Challenge: Reusable Section Box

Create a TitledBox view that takes a title string and a @ViewBuilder content closure, showing the title above whatever views the caller passes. Then write a ViewModifier called RoundedBordered and expose it as .roundedBordered(). Use both together to build a tidy settings panel.

🎉 Lesson Complete!

Practice quiz

What is a custom ViewModifier?

  • A new View
  • A reusable type that packages a set of modifications applied to a view
  • A property wrapper
  • A navigation link

Answer: A reusable type that packages a set of modifications applied to a view. A ViewModifier bundles modifications so you can reuse a consistent style across many views.

What method must a ViewModifier implement?

  • body(content:)
  • render()
  • draw()
  • init()

Answer: body(content:). A ViewModifier implements body(content:), returning the modified content.

What is the 'content' parameter in body(content:)?

  • A string
  • The view the modifier is applied to
  • A color
  • The parent view

Answer: The view the modifier is applied to. content is the view being modified; you return it wrapped with your changes.

How do you apply a custom ViewModifier to a view?

  • view.apply(MyModifier())
  • MyModifier(view)
  • view.modifier(MyModifier())
  • view + MyModifier()

Answer: view.modifier(MyModifier()). Call .modifier(MyModifier()) to apply a custom ViewModifier to any view.

What is a common way to make a modifier nicer to call?

  • Add a View extension method that calls .modifier(...)
  • Subclass View
  • Use a global function only
  • It cannot be improved

Answer: Add a View extension method that calls .modifier(...). An extension on View exposing a convenience method (e.g. .cardStyle()) reads cleanly.

What does @ViewBuilder do?

  • Builds the app
  • Compiles code
  • Stores state
  • Lets a closure return multiple child views (and use if/switch)

Answer: Lets a closure return multiple child views (and use if/switch). @ViewBuilder enables a closure to combine several views and use conditionals to build content.

Why can a VStack's trailing closure contain several views?

  • Because of @ViewBuilder on its content parameter
  • Because of @State
  • Because of @Binding
  • By accident

Answer: Because of @ViewBuilder on its content parameter. Container initializers mark their content closure @ViewBuilder, allowing multiple child views.

What is a clean way to reuse a chunk of UI?

  • Copy and paste it
  • Extract it into its own View struct (or a computed property)
  • Use a global variable
  • Inline everything

Answer: Extract it into its own View struct (or a computed property). Extracting repeated UI into a dedicated View struct keeps code DRY and readable.

Custom modifiers are most useful when you want to...

  • Apply the same group of styling repeatedly and consistently
  • Avoid using SwiftUI
  • Replace the View protocol
  • Disable previews

Answer: Apply the same group of styling repeatedly and consistently. They centralize a reusable style so it stays consistent and easy to change in one place.

A function or property marked @ViewBuilder can return what?

  • Only one view, ever
  • Several views combined into one result, with conditionals allowed
  • Only strings
  • Only numbers

Answer: Several views combined into one result, with conditionals allowed. @ViewBuilder composes the returned views into a single result and supports if/switch branches.

Continue this course

Frequently asked questions

What is a custom ViewModifier?

A custom ViewModifier is a type conforming to the ViewModifier protocol that packages a reusable set of view changes. You implement body(content:), where content is the view being modified, and return it wrapped with your modifications (padding, background, etc.). You apply it with .modifier(MyModifier()), centralizing a style so it's consistent and easy to change in one place.

How do I make a custom modifier read like a built-in one?

Add an extension on View with a convenience method that calls .modifier(...). For example, extension View { func cardStyle() -> some View { modifier(CardStyle()) } }. Then you write .cardStyle() instead of .modifier(CardStyle()), which is cleaner and matches how SwiftUI's own modifiers look at the call site.

What is @ViewBuilder?

@ViewBuilder is a result builder that lets a closure or function return multiple child views combined into one result, and use control flow like if and switch. It's why a VStack's trailing closure can contain several views: the content parameter is marked @ViewBuilder. You can mark your own closures and computed properties with it to build flexible, composable content.

When should I extract a view versus write a modifier?

Extract a view (a new View struct) when you're repeating a chunk of UI structure — a card, a row, a tile. Write a ViewModifier when you're repeating a set of styling changes you want to apply to different views. Often you use both: an extracted StatTile view that applies a .cardStyle() modifier.

Does a modifier create a new view each time?

Yes. Like built-in modifiers, applying a ViewModifier returns a new view that wraps the original with your changes; it doesn't mutate the original in place. Because views are lightweight value types, this is cheap, and it keeps your styling composable and predictable.