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
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.

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