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
- • Build a reusable ViewModifier with body(content:)
- • Apply it with .modifier(...)
- • Expose a clean API via a View extension
- • Accept multiple children with @ViewBuilder
- • Extract repeated UI into reusable View structs
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
- 💡 Wrap modifiers in a View extension so call sites read like built-ins.
- 💡 Modifier vs view. Reuse styling with a modifier; reuse structure by extracting a view.
- 💡 @ViewBuilder for flexible APIs. Let callers pass any number of child views.
Common Errors (and the fix)
- "Type 'CardStyle' does not conform to 'ViewModifier'" — implement func body(content: Content) -> some View.
- "Closure containing multiple statements..." in a custom container — mark the content closure @ViewBuilder.
- Copy-pasted styling drifts out of sync — centralize it in one modifier and call it everywhere.
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!
- ✅ A ViewModifier packages reusable styling via body(content:)
- ✅ Apply with .modifier(...); expose cleanly via a View extension
- ✅ @ViewBuilder lets closures return multiple views and use conditionals
- ✅ Extract repeated UI into reusable View structs
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
- Previous: Animations & Transitions
- Next: Introduction to Combine — Publishers, subscribers, @Published, sink and AnyCancellable
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.