SwiftUI Custom ViewModifier — Define Once, Use Everywhere

Originally published on Medium in July 2024.

Five buttons, five times the same modifier chain. That is copy-paste development, and there is a better way.


SwiftUI makes it frighteningly easy to build beautiful views — and just as easy to repeat yourself in the process. A button with .font(.headline), .foregroundStyle(.white), .background(.pink), .cornerRadius(16) looks great. By the third time you type it, it gets tedious. By the fifth, it becomes a maintenance problem because every design change has to happen in five places at once.

The solution is simple: an extension on View that bundles all modifiers.

The Extension

extension View {
    public func myAmazingModifier(bgColor: Color = .pink) -> some View {
        self
            .font(.headline)
            .foregroundStyle(.white)
            .frame(maxWidth: .infinity)
            .frame(height: 50)
            .background(bgColor)
            .cornerRadius(16)
    }
}

A default parameter for the background color, everything else is fixed. Changes to font, height, or corner radius take effect everywhere immediately.

Before vs. After

Before — every view carries the full modifier chain:

Text("First Button")
    .font(.headline)
    .foregroundStyle(.white)
    .frame(maxWidth: .infinity)
    .frame(height: 50)
    .background(.pink)
    .cornerRadius(16)

Single button with manual modifiers

After — a one-liner per button:

Text("First Button")
    .myAmazingModifier()

Text("Second Button")
    .myAmazingModifier(bgColor: .yellow)

Text("Third Button")
    .myAmazingModifier(bgColor: .blue)

Five buttons, one definition. The color is overridden per call, everything else stays consistent.

Five buttons with the same custom modifier in different colors


Extension vs. ViewModifier Protocol

SwiftUI also offers the ViewModifier protocol with func body(content: Content) -> some View. Which approach to choose depends on whether the modifier needs its own state.

ApproachWhen to use
View ExtensionSimple modifier chains without their own state
ViewModifier ProtocolWhen the modifier needs its own @State, @Environment, or lifecycle hooks

For everyday use, the extension is enough. The protocol only makes sense when the modifier itself needs to think — for example, a shake animation modifier with its own animation state.


When three lines are enough, you do not need a protocol.


Translated with the help of Claude