SwiftUIiOS DevelopmentDesign Systems

Custom SwiftUI View Modifiers for iOS Design Systems

Step-by-step guide to building reusable SwiftUI view modifiers that enforce design consistency and reduce code duplication in iOS apps.

Andrew Vikuk

Andrew Vikuk

6 min read1,168 words

When I was building Grown, my SwiftUI learning platform, I noticed I was copy-pasting the same styling code everywhere. Card shadows, button styles, loading states — all scattered across dozens of views. That's when I dove deep into custom SwiftUI view modifiers, and honestly, they transformed how I approach iOS app design systems.

Let me walk you through creating reusable view modifiers that'll make your code cleaner and your design more consistent.

Why Custom View Modifiers Matter

Before jumping into code, here's why this matters. In a typical iOS app, you'll have buttons, cards, input fields, and other components that should look consistent. Without a system, you end up with:

  • Duplicate styling code scattered everywhere
  • Inconsistent spacing and colors
  • Nightmare maintenance when designs change
  • New team members reinventing the wheel

Custom SwiftUI view modifiers solve this by encapsulating your design decisions into reusable, composable pieces.

Prerequisites

You'll need:

  • Xcode 14+ (though most of this works on earlier versions)
  • Basic SwiftUI knowledge — you should understand views and modifiers
  • A project to experiment with (create a new one if needed)

Setting Up Your Design System Foundation

First, let's create the foundation. I always start with a DesignSystem enum to centralize colors, spacing, and typography:

enum DesignSystem {
    enum Colors {
        static let primary = Color.blue
        static let secondary = Color.gray
        static let surface = Color(.systemBackground)
        static let onSurface = Color(.label)
        static let shadow = Color.black.opacity(0.1)
    }
    
    enum Spacing {
        static let xs: CGFloat = 4
        static let sm: CGFloat = 8
        static let md: CGFloat = 16
        static let lg: CGFloat = 24
        static let xl: CGFloat = 32
    }
    
    enum Corner {
        static let sm: CGFloat = 8
        static let md: CGFloat = 12
        static let lg: CGFloat = 16
    }
}

This approach saved me countless hours on Grown. When I needed to adjust spacing across the entire app, I changed one value instead of hunting through dozens of files.

Creating Your First Custom View Modifier

Let's start with something every app needs: a card style. Here's how to create a custom SwiftUI view modifier:

struct CardModifier: ViewModifier {
    func body(content: Content) -> some View {
        content
            .background(DesignSystem.Colors.surface)
            .cornerRadius(DesignSystem.Corner.md)
            .shadow(
                color: DesignSystem.Colors.shadow,
                radius: 8,
                x: 0,
                y: 2
            )
            .padding(.horizontal, DesignSystem.Spacing.md)
    }
}

// Extension to make it easier to use
extension View {
    func cardStyle() -> some View {
        modifier(CardModifier())
    }
}

Now instead of repeating that styling code, you just use:

VStack {
    Text("Hello, World!")
    Text("This is a card")
}
.cardStyle()

Building More Complex Modifiers

Let's create something more sophisticated — a button modifier that handles different states:

struct PrimaryButtonModifier: ViewModifier {
    let isLoading: Bool
    let isDisabled: Bool
    
    func body(content: Content) -> some View {
        content
            .font(.system(size: 16, weight: .semibold))
            .foregroundColor(.white)
            .frame(maxWidth: .infinity)
            .frame(height: 48)
            .background(backgroundColor)
            .cornerRadius(DesignSystem.Corner.sm)
            .opacity(isDisabled ? 0.6 : 1.0)
            .overlay(loadingOverlay)
            .animation(.easeInOut(duration: 0.2), value: isLoading)
    }
    
    private var backgroundColor: Color {
        if isDisabled {
            return DesignSystem.Colors.secondary
        }
        return DesignSystem.Colors.primary
    }
    
    @ViewBuilder
    private var loadingOverlay: some View {
        if isLoading {
            ProgressView()
                .progressViewStyle(CircularProgressViewStyle(tint: .white))
        }
    }
}

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

This modifier handles multiple button states in one place. When I was building the authentication flow for Focus Ninja, having consistent button states across login, signup, and password reset screens made the UX feel much more polished.

iOS App Design System Implementation

Here's where it gets interesting. Let's build a comprehensive system with multiple modifier types:

Text Style Modifiers

struct HeadlineModifier: ViewModifier {
    func body(content: Content) -> some View {
        content
            .font(.system(size: 24, weight: .bold))
            .foregroundColor(DesignSystem.Colors.onSurface)
    }
}

struct BodyModifier: ViewModifier {
    func body(content: Content) -> some View {
        content
            .font(.system(size: 16, weight: .regular))
            .foregroundColor(DesignSystem.Colors.onSurface)
            .lineSpacing(2)
    }
}

extension View {
    func headlineStyle() -> some View {
        modifier(HeadlineModifier())
    }
    
    func bodyStyle() -> some View {
        modifier(BodyModifier())
    }
}

Input Field Modifiers

struct TextFieldModifier: ViewModifier {
    let isError: Bool
    
    func body(content: Content) -> some View {
        content
            .padding(DesignSystem.Spacing.md)
            .background(DesignSystem.Colors.surface)
            .cornerRadius(DesignSystem.Corner.sm)
            .overlay(
                RoundedRectangle(cornerRadius: DesignSystem.Corner.sm)
                    .stroke(borderColor, lineWidth: 1)
            )
    }
    
    private var borderColor: Color {
        isError ? .red : DesignSystem.Colors.secondary.opacity(0.3)
    }
}

extension View {
    func textFieldStyle(isError: Bool = false) -> some View {
        modifier(TextFieldModifier(isError: isError))
    }
}

Advanced Techniques: Conditional Modifiers

Sometimes you need modifiers that adapt to different contexts. Here's a technique I use frequently:

struct AdaptiveCardModifier: ViewModifier {
    let style: CardStyle
    
    enum CardStyle {
        case elevated
        case flat
        case outlined
    }
    
    func body(content: Content) -> some View {
        content
            .background(DesignSystem.Colors.surface)
            .cornerRadius(DesignSystem.Corner.md)
            .modifier(StyleSpecificModifier(style: style))
    }
}

struct StyleSpecificModifier: ViewModifier {
    let style: AdaptiveCardModifier.CardStyle
    
    func body(content: Content) -> some View {
        switch style {
        case .elevated:
            content.shadow(
                color: DesignSystem.Colors.shadow,
                radius: 8,
                x: 0,
                y: 4
            )
        case .flat:
            content
        case .outlined:
            content.overlay(
                RoundedRectangle(cornerRadius: DesignSystem.Corner.md)
                    .stroke(DesignSystem.Colors.secondary.opacity(0.3), lineWidth: 1)
            )
        }
    }
}

Common Pitfalls to Avoid

I've made these mistakes so you don't have to:

Overcomplicating modifiers: Start simple. I initially tried to create one mega-modifier that handled everything. Bad idea. Keep them focused on single responsibilities.

Ignoring performance: View modifiers can impact performance if they're too complex. Profile your app if you notice slowdowns.

Not considering accessibility: Always include accessibility support in your modifiers:

struct AccessibleButtonModifier: ViewModifier {
    let label: String
    let hint: String?
    
    func body(content: Content) -> some View {
        content
            .accessibilityLabel(label)
            .accessibilityHint(hint ?? "")
            .accessibilityAddTraits(.isButton)
    }
}

Forgetting about Dark Mode: Test your modifiers in both light and dark modes. Use semantic colors when possible.

Organizing Your Modifier Files

In larger projects like Grown, organization matters. I structure my modifiers like this:

DesignSystem/
├── Core/
│   ├── Colors.swift
│   ├── Spacing.swift
│   └── Typography.swift
├── Modifiers/
│   ├── ButtonModifiers.swift
│   ├── CardModifiers.swift
│   ├── TextModifiers.swift
│   └── InputModifiers.swift
└── Components/
    ├── CustomButton.swift
    └── CustomCard.swift

This makes it easy for team members to find and contribute to the design system.

Testing Your Design System

Create a preview that showcases all your modifiers:

struct DesignSystemPreview: View {
    var body: some View {
        ScrollView {
            VStack(spacing: DesignSystem.Spacing.lg) {
                Text("Headline")
                    .headlineStyle()
                
                Text("Body text with proper spacing and line height")
                    .bodyStyle()
                
                Button("Primary Button") {}
                    .primaryButton()
                
                VStack {
                    Text("Card Content")
                }
                .cardStyle()
            }
            .padding()
        }
    }
}

#Preview {
    DesignSystemPreview()
}

Next Steps

Once you've built your basic modifier system, consider:

  • Creating compound modifiers that combine multiple styles
  • Building theme support for different app sections
  • Adding animation modifiers for consistent micro-interactions
  • Documenting your system for team members

Custom SwiftUI view modifiers have become essential to my iOS development workflow. They make code more maintainable, designs more consistent, and development faster once the initial investment is made.

If you're building an iOS app and want help implementing a solid design system from the start, I'd be happy to chat about your project. Sometimes having an experienced developer guide the architecture decisions early can save weeks of refactoring later.

Andrew Vikuk

Need help building your app or website?

I design and develop iOS apps and modern websites from concept to launch. Let's talk about your project.

Get in touch