Swift and SwiftUI tutorials for Swift Developers

How to create a custom modifier in SwiftUI

If you have been immersed in Swift programming and SwiftUI development for a while, you have surely encountered the dreaded “Modifier Hell.” You are building an incredible view in Xcode, but your code starts to look like an endless shopping list: .padding().background().cornerRadius().shadow().font().foregroundColor()… repeated over and over again for every button, card, or text field in your application.

For a professional iOS developer, the DRY (Don’t Repeat Yourself) principle is sacred. Copy-pasting style blocks not only clutters the code but turns app maintenance into a nightmare. If the designer decides to change the corner radius from 10 to 12, are you going to search through 50 different files?

The elegant solution Apple offers us is to create a custom modifier (ViewModifier).

In this comprehensive tutorial, you will learn how to encapsulate styles and reusable logic, cleaning up your code and creating your own agnostic design system that works across iOS, macOS, and watchOS. We will elevate your SwiftUI level from “user” to “architect.”


What is really a ViewModifier?

At the heart of SwiftUI, modifiers are not magic; they are structures. When you write .padding(), you aren’t changing a property of a view (as you would in UIKit with UIView); you are wrapping your current view in a new view that adds that padding.

The ViewModifier protocol is the interface that allows us to intervene in this construction process. Its definition is deceptively simple:

protocol ViewModifier {
    associatedtype Body : View
    func body(content: Self.Content) -> Self.Body
}

The body function receives the content (the view we are modifying) and returns a new view (Body). Your job as an iOS developer is to take that content, apply transformations to it, and return the result.


Step 1: Your First Custom Modifier

Let’s start with a classic use case: The “Card View” design. We want multiple containers in our app to have a white background, rounded corners, and a soft shadow.

The Implementation

Open Xcode and create a new Swift file named CardModifier.swift.

import SwiftUI

struct CardModifier: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(Color(.systemBackground))
            .cornerRadius(12)
            .shadow(color: Color.black.opacity(0.1), radius: 4, x: 0, y: 2)
    }
}

How to use it

To apply this modifier, we use the .modifier(_:) method:

Text("Hello, SwiftUI")
    .modifier(CardModifier())

Although it works, this syntax isn’t very “Swifty.” Apple’s native modifiers are written like .padding(), not .modifier(Padding()). To reach that level of elegance in our Swift programming, we need to go one step further.


Step 2: The Magic of Extensions (Syntactic Sugar)

To make our custom modifier feel like a first-class citizen in the ecosystem, we must extend the View protocol. This not only improves readability but also facilitates autocomplete in Xcode.

Add this to the end of your CardModifier.swift file:

extension View {
    func cardStyle() -> some View {
        self.modifier(CardModifier())
    }
}

Now, your code in the main view will be clean and semantic:

VStack {
    Text("Dashboard")
        .font(.title)
    
    VStack {
        Text("User Stats")
        Text("Sales: 120%")
    }
    .cardStyle() // Much cleaner!
}

This small change differentiates a junior developer from an iOS developer who cares about their code’s API.


Step 3: Modifiers with State and Parameters

Custom modifiers are not limited to static styles. They can accept parameters and even maintain their own state (@State), making them incredibly powerful for encapsulating UI logic.

Imagine you want to create a button that, when pressed, performs a “bounce” animation (scale effect). Instead of writing the animation logic in every button of your app, let’s encapsulate it.

The BounceButtonModifier

struct BounceButtonModifier: ViewModifier {
    var bounceScale: CGFloat
    @State private var isPressed = false
    
    func body(content: Content) -> some View {
        content
            .scaleEffect(isPressed ? bounceScale : 1.0)
            .animation(.spring(response: 0.3, dampingFraction: 0.5), value: isPressed)
            .gesture(
                DragGesture(minimumDistance: 0)
                    .onChanged { _ in isPressed = true }
                    .onEnded { _ in isPressed = false }
            )
    }
}

extension View {
    func bounceEffect(scale: CGFloat = 0.9) -> some View {
        self.modifier(BounceButtonModifier(bounceScale: scale))
    }
}

Technical Analysis

  1. Parameters: We accept bounceScale so the developer can decide how much the button shrinks.
  2. Internal State: We use @State private var isPressed inside the modifier. This is crucial: the modifier manages its own logic. The parent view doesn’t need to know anything about the animation state.
  3. Encapsulation: All gesture and animation logic lives inside the modifier.

Now you can apply .bounceEffect() to any image, text, or container, granting it complex interactivity with a single line of code.


Multiplatform Adaptability: iOS, macOS, and watchOS

One of the great promises of SwiftUI is “Learn once, apply anywhere.” However, an iOS developer knows that design is not the same on an iPhone as it is on an Apple Watch.

When you create a custom modifier, you can use conditional compilation or environment logic to adapt the style according to the operating system.

Example: Adaptive Button Style

Suppose you want a primary button that is large and blue on iOS, but subtler and bordered on macOS.

struct AdaptivePrimaryButton: ViewModifier {
    func body(content: Content) -> some View {
        #if os(iOS)
        content
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(10)
        #elseif os(macOS)
        content
            .padding(8)
            .background(Color.blue.opacity(0.1))
            .foregroundColor(.blue)
            .cornerRadius(6)
            .overlay(
                RoundedRectangle(cornerRadius: 6)
                    .stroke(Color.blue, lineWidth: 1)
            )
        #else
        // watchOS, tvOS
        content
            .foregroundColor(.blue)
        #endif
    }
}

This allows you to centralize design decisions. If the design team changes the button style on macOS, you only modify this file, and your entire application updates.


Advanced SwiftUI: ViewModifier vs. View Extension

A common question in iOS developer interviews is: When should I create a ViewModifier struct and when is a simple View extension enough?

Option A: Direct Extension (No Struct)

extension View {
    func redTitle() -> some View {
        self
            .font(.title)
            .foregroundColor(.red)
    }
}

Option B: ViewModifier (With Struct)

struct RedTitleModifier: ViewModifier { ... }

The Golden Rule:

  1. Use Direct Extension when you are only combining existing SwiftUI modifiers and don’t need state variables (@State) or complex stored properties. It is lighter for the compiler.
  2. Use ViewModifier (Struct) when you need to:
    • Maintain state (@State@Binding).
    • Access the @Environment (e.g., to detect dark mode or dynamic font size).
    • Reuse complex logic that transforms the Content in non-standard ways.

Optimizing for Performance in Xcode

SwiftUI is very efficient, but the abuse of complex modifiers can impact rendering performance or compile time.

The .concat() Modifier

If you have two modifiers that always go together, you can combine them before applying them to the view.

let baseStyle = CardModifier()
let interactionStyle = BounceButtonModifier(bounceScale: 0.95)
let combinedStyle = baseStyle.concat(interactionStyle)

Text("Click Me")
    .modifier(combinedStyle)

This is useful when you are building a Design System and want to compose complex styles from simpler atoms.

Debugging Hierarchies

When you use many custom modifiers, the view hierarchy in the Xcode “View Debugger” can become deep. Remember that each modifier wraps the previous view.

Pro Tip: Use the .id("IdentifiableName") modifier on your key views during development to easily find them in Xcode’s visual inspector if the hierarchy becomes confusing.


Real Use Case: Watermark Modifier

To finish this tutorial, let’s create something that would be difficult to do by simply copy-pasting styles: a modifier that injects a watermark over any view, ideal for photography apps or trial versions.

struct Watermark: ViewModifier {
    var text: String
    
    func body(content: Content) -> some View {
        ZStack(alignment: .bottomTrailing) {
            content
            
            Text(text)
                .font(.caption)
                .foregroundColor(.white)
                .padding(5)
                .background(Color.black.opacity(0.5))
                .cornerRadius(4)
                .padding(10) // Margin from the corner
        }
    }
}

extension View {
    func watermarked(with text: String) -> some View {
        self.modifier(Watermark(text: text))
    }
}

Why is this cool? Notice that we use a ZStack. The modifier not only changes attributes of the original view (content) but changes its structure, placing it inside a stack and adding a new view (Text) on top.

This demonstrates the true power of creating a custom modifier: you have full control over the visual hierarchy surrounding your content.


Conclusion

The transition from writing SwiftUI code “that works” to writing SwiftUI code “that scales” passes through mastering ViewModifier.

As an iOS developer, your goal should be to create code that is readable like an English sentence. By encapsulating styles and behaviors in semantic modifiers (.cardStyle().bounceEffect().watermarked()), you reduce cognitive load when reading the code and centralize your interface logic.

Summary of benefits:

  1. DRY: Write once, use everywhere.
  2. Maintainability: Design changes in a single point.
  3. Readability: Clean your main views of configuration “noise.”
  4. Consistency: Makes it easy for the whole team to use the same predefined styles.

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

How to animate text in SwiftUI

Next Article

SwiftUI Xcode Keyboard Shortcuts PDF

Related Posts