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

SwiftUI modifiers: Swift & iOS best practices explained

Order changes results.

Expona Admin7 min read

SwiftUI modifiers: order matters more than you think

We've all built a button that looked perfect in isolation but fell apart when we reused it across screens. Modifier order in SwiftUI is one of those seemingly small decisions that compounds into maintainability headaches across your codebase. Understanding how to layer modifiers—and when to extract them into custom types—separates production-grade apps from prototypes.

SwiftUI modifiers: Swift & iOS best practices explained comes down to one core principle: modifiers create a new view by wrapping the previous one. The order you apply them determines the final layout, spacing, and interaction behavior. Get this wrong, and you'll debug mysterious spacing bugs or find that your tap targets don't work as expected. Get it right, and your views become composable, testable, and easier for teammates to understand.

Padding then background differs from the reverse

This is the most common source of confusion. When you write Text("Hello").padding().background(Color.blue), you're applying padding to the text first, then drawing a blue background around the padded area. The background expands to fit the padded content.

Reverse the order: Text("Hello").background(Color.blue).padding(). Now the background is applied to the original text (no padding inside the background), and then padding is added around the entire background view. The visual result is dramatically different.

Let's see this in action with a real example from a booking confirmation screen:

swift
// Approach 1: padding inside background
VStack(spacing: 16) {
    Text("Booking Confirmed")
        .font(.headline)
        .padding(12)
        .background(Color.green)
        .cornerRadius(8)
    
    Text("Your ride is arriving in 5 minutes")
        .font(.body)
        .padding(12)
        .background(Color.green.opacity(0.1))
        .cornerRadius(8)
}
.padding(16)

Here, each text view gets internal padding, then a background is drawn around that padded area. The corners round nicely because the background view itself is being rounded.

Now compare:

swift
// Approach 2: background inside padding
VStack(spacing: 16) {
    Text("Booking Confirmed")
        .font(.headline)
        .background(Color.green)
        .cornerRadius(8)
        .padding(12)
    
    Text("Your ride is arriving in 5 minutes")
        .font(.body)
        .background(Color.green.opacity(0.1))
        .cornerRadius(8)
        .padding(12)
}
.padding(16)

The second approach produces tighter spacing between text and background because the padding happens after. The visual distinction becomes critical when designing card-based layouts or status badges.

In production, we've found this rule works well: apply padding first to control internal spacing, then apply background styling. This matches how designers think about components: define the content area, then add visual treatment around it.

Custom modifiers with ViewModifier

When you find yourself repeating the same modifier chain across five screens, that's your signal to extract it. SwiftUI's ViewModifier protocol lets you encapsulate modifier logic into reusable types.

Here's where many teams go wrong: they create a modifier and immediately make it overly generic. Start specific. Build for one use case, then generalize when patterns emerge.

Let's build a custom modifier for error message displays, based on patterns we see in e-commerce checkout flows:

swift
struct ErrorMessageModifier: ViewModifier {
    func body(content: Content) -> some View {
        content
            .font(.system(.caption, design: .default))
            .foregroundColor(.white)
            .padding(8)
            .background(Color.red)
            .cornerRadius(6)
    }
}

extension View {
    func errorMessage() -> some View {
        modifier(ErrorMessageModifier())
    }
}

// Usage
Text("Payment method invalid")
    .errorMessage()

This is clean, but it's also locked into red with specific padding. A month later, you need a warning style in amber. Now you're tempted to add parameters and branching logic. Resist that. Instead, create separate, focused modifiers:

swift
struct ErrorMessageModifier: ViewModifier {
    let backgroundColor: Color
    let padding: CGFloat
    
    init(backgroundColor: Color = .red, padding: CGFloat = 8) {
        self.backgroundColor = backgroundColor
        self.padding = padding
    }
    
    func body(content: Content) -> some View {
        content
            .font(.system(.caption, design: .default))
            .foregroundColor(.white)
            .padding(padding)
            .background(backgroundColor)
            .cornerRadius(6)
    }
}

extension View {
    func errorMessage(
        backgroundColor: Color = .red,
        padding: CGFloat = 8
    ) -> some View {
        modifier(ErrorMessageModifier(backgroundColor: backgroundColor, padding: padding))
    }
}

// Usage
Text("Payment method invalid").errorMessage()
Text("This offer expires soon").errorMessage(backgroundColor: .orange, padding: 12)

The second version is parameterized but still intentional. You're not trying to build a kitchen-sink modifier that handles every edge case. You're making one thing do its job well.

Keep modifier chains readable

Chain readability breaks when you stack more than about five modifiers vertically. At that point, you're making maintenance harder for minimal benefit. Junior engineers will read `.padding(12).background(Color.gray).opacity(0.8).cornerRadius(6)` and understand it. Add fifteen more modifiers, and everyone slows down.

The strategy we use: group related modifiers conceptually, even if they're technically separate calls. Use intermediate variables when a view will be reused:

swift
// Hard to read
Button(action: { }) {
    Text("Submit Payment")
}
.font(.headline)
.foregroundColor(.white)
.padding(12)
.background(Color.blue)
.cornerRadius(8)
.shadow(radius: 4)
.padding(16)
.disabled(!isFormValid)
.opacity(isFormValid ? 1.0 : 0.5)
.scaleEffect(isPressed ? 0.95 : 1.0, anchor: .center)

// More readable
let buttonContent = Button(action: { }) {
    Text("Submit Payment")
}
.font(.headline)
.foregroundColor(.white)

let buttonStyling = buttonContent
    .padding(12)
    .background(Color.blue)
    .cornerRadius(8)
    .shadow(radius: 4)

let buttonInteraction = buttonStyling
    .padding(16)
    .disabled(!isFormValid)
    .opacity(isFormValid ? 1.0 : 0.5)
    .scaleEffect(isPressed ? 0.95 : 1.0, anchor: .center)

The second version breaks the chain into semantic groups: content styling, visual treatment, then interaction state. A teammate reading this immediately understands the layers without counting parentheses.

Extract repeated chains

After three or four uses of the same modifier sequence across your codebase, extract it. This isn't dogma—it's practical economics. Every repeated sequence is a maintenance liability. When the design team decides all primary buttons need a different corner radius, you want to change it in one place.

We typically identify candidates by code review. If a reviewer has to check three different files to verify that button styling is consistent, that's a sign we should have extracted it.

Here's a realistic scenario from a ride-hailing app:

swift
// Before: repeated in RideDetailsView, HistoryView, BookingConfirmView
Button(action: confirmRide) {
    HStack {
        Image(systemName: "checkmark.circle.fill")
        Text("Confirm Ride")
    }
}
.font(.system(.body, design: .default))
.fontWeight(.semibold)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(12)
.background(Color.green)
.cornerRadius(8)
.disabled(!canConfirm)

// After: extracted into a custom modifier and helper
struct PrimaryButtonModifier: ViewModifier {
    let isDisabled: Bool
    
    func body(content: Content) -> some View {
        content
            .font(.system(.body, design: .default))
            .fontWeight(.semibold)
            .foregroundColor(.white)
            .frame(maxWidth: .infinity)
            .padding(12)
            .background(isDisabled ? Color.gray : Color.green)
            .cornerRadius(8)
            .disabled(isDisabled)
    }
}

extension View {
    func primaryButton(isDisabled: Bool = false) -> some View {
        modifier(PrimaryButtonModifier(isDisabled: isDisabled))
    }
}

// Now use consistently everywhere
Button(action: confirmRide) {
    HStack {
        Image(systemName: "checkmark.circle.fill")
        Text("Confirm Ride")
    }
}
.primaryButton(isDisabled: !canConfirm)

The extracted version is shorter, testable (you can verify styling in isolation), and updates propagate instantly. When accessibility requirements change or iOS releases demand new padding standards, you update once.

Common mistakes

Assuming modifier order doesn't matter: It always matters. The order determines which view wraps which, and that controls layout calculations. A background applied before padding looks dramatically different from padding applied before background.

Creating overly generic custom modifiers too early: Generic modifiers with five parameters are harder to maintain than three specific ones. Extract when you see repetition, not when you anticipate future use cases.

Applying modifiers in the wrong scope: Putting modifiers outside a group when they should be inside (or vice versa) breaks layout. Always think about which view you're actually modifying. .padding() on a VStack pads the entire stack; .padding() on individual children pads each child separately.

Ignoring readability for brevity: A single long modifier chain might be fewer lines, but it's harder to debug. Breaking chains into intermediate variables costs nothing and gains clarity. Future you will thank current you when you're hunting a layout bug at 10 PM.

Forgetting that modifiers are views: Custom modifiers don't just apply styling—they return a view. That view can have its own behavior, animation, and state. Treating them as simple cosmetic wrappers limits what you can do. They're more powerful than most teams realize.

Key takeaways

  • Modifier order directly affects layout and appearance. Padding before background differs visually from background before padding. Think about which view wraps which.
  • Extract repeated modifier chains into custom ViewModifier types once you see them used three or more times. This centralizes styling decisions and makes updates trivial.
  • Keep individual modifier chains readable by grouping related modifiers conceptually and using intermediate variables when chains exceed five or six calls.
  • Custom modifiers should be specific to their purpose initially. Parameterize them when patterns emerge, but resist building generic catch-all modifiers that try to handle every case.
  • Apply padding first to control internal spacing, then apply background and styling. This matches design thinking and makes layouts more predictable.
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