# SwiftUI Custom ViewModifiers: Reusable Patterns for Design Systems > Build custom SwiftUI ViewModifiers for a consistent design system. Patterns, best practices, and practical examples to style iOS views efficiently. - Published: 2026-03-01 - Updated: 2026-03-28 - Author: SharpSkill - Tags: swiftui, ios, viewmodifier, design-system, swift - Reading time: 12 min --- ViewModifiers form the foundation of a robust and maintainable SwiftUI design system. By encapsulating styles and behaviors into reusable components, they ensure visual consistency while drastically reducing code duplication. This modular approach transforms how modern iOS interfaces get built. > **What this article covers** > > This article presents essential patterns for creating effective custom ViewModifiers, from basic structure to advanced compositions for a complete design system. ## Anatomy of a ViewModifier A ViewModifier is a protocol that defines a transformation applicable to any view. Unlike View extensions, ViewModifiers can maintain internal state and accept configurable parameters. ```swift // CardModifier.swift import SwiftUI // Basic structure of a custom ViewModifier struct CardModifier: ViewModifier { // Configurable parameters var cornerRadius: CGFloat = 12 var shadowRadius: CGFloat = 4 // The body method applies transformations func body(content: Content) -> some View { content .padding() .background(Color(.systemBackground)) .cornerRadius(cornerRadius) .shadow( color: .black.opacity(0.1), radius: shadowRadius, x: 0, y: 2 ) } } // Extension for fluent syntax extension View { func cardStyle( cornerRadius: CGFloat = 12, shadowRadius: CGFloat = 4 ) -> some View { modifier(CardModifier( cornerRadius: cornerRadius, shadowRadius: shadowRadius )) } } ``` The `Content` pattern represents the view on which the modifier applies. This abstraction allows the same modifier to work on any type of SwiftUI view. ### Using the CardModifier in Practice Once created, applying the modifier becomes intuitive and reads naturally in code: ```swift // CardUsageExample.swift struct ProductCard: View { let product: Product var body: some View { VStack(alignment: .leading, spacing: 8) { // Product image AsyncImage(url: product.imageURL) { image in image .resizable() .aspectRatio(contentMode: .fill) } placeholder: { Rectangle() .fill(Color.gray.opacity(0.2)) } .frame(height: 150) .clipped() // Product information Text(product.name) .font(.headline) Text(product.price, format: .currency(code: "USD")) .foregroundStyle(.secondary) } .cardStyle() // Applying the modifier } } struct Product: Identifiable { let id: UUID let name: String let price: Decimal let imageURL: URL? } ``` Code clarity significantly improves maintainability. Changing the style of all cards in the application requires only a single change in the ViewModifier. ## Conditional Modifiers with @ViewBuilder ViewModifiers can include conditional logic to adapt their behavior based on context. The `@ViewBuilder` attribute enables building complex views with branching. ```swift // ConditionalBadgeModifier.swift struct BadgeModifier: ViewModifier { let count: Int let color: Color let showZero: Bool init(count: Int, color: Color = .red, showZero: Bool = false) { self.count = count self.color = color self.showZero = showZero } // @ViewBuilder enables conditions in the body @ViewBuilder func body(content: Content) -> some View { if count > 0 || showZero { content.overlay(alignment: .topTrailing) { badgeView } } else { // Returns the view without modification content } } // Badge view extracted for readability private var badgeView: some View { Text(count > 99 ? "99+" : "\(count)") .font(.caption2.bold()) .foregroundStyle(.white) .padding(.horizontal, 6) .padding(.vertical, 2) .background(color) .clipShape(Capsule()) .offset(x: 8, y: -8) } } extension View { func badge( count: Int, color: Color = .red, showZero: Bool = false ) -> some View { modifier(BadgeModifier( count: count, color: color, showZero: showZero )) } } ``` This pattern enables adding notification badges to any interface element in a declarative and configurable manner. > **Performance of conditionals** > > SwiftUI automatically optimizes `@ViewBuilder` branches. The non-displayed view doesn't render, preserving performance even with complex conditions. ## Design System with Style Tokens A mature design system relies on tokens: semantic constants that define the application's visual identity. ViewModifiers perfectly encapsulate these tokens. ```swift // DesignTokens.swift import SwiftUI // Spacing tokens 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 } // Radius tokens enum Radius { static let sm: CGFloat = 4 static let md: CGFloat = 8 static let lg: CGFloat = 16 static let full: CGFloat = 9999 } // Typography tokens enum Typography { static let displayLarge = Font.system(size: 34, weight: .bold) static let displayMedium = Font.system(size: 28, weight: .bold) static let titleLarge = Font.system(size: 22, weight: .semibold) static let titleMedium = Font.system(size: 17, weight: .semibold) static let bodyLarge = Font.system(size: 17, weight: .regular) static let bodyMedium = Font.system(size: 15, weight: .regular) static let caption = Font.system(size: 12, weight: .regular) } // Semantic color tokens enum SemanticColor { static let primary = Color("PrimaryColor") static let secondary = Color("SecondaryColor") static let success = Color.green static let warning = Color.orange static let error = Color.red static let surface = Color(.systemBackground) static let surfaceVariant = Color(.secondarySystemBackground) } ``` ### Token-Based ViewModifiers These tokens integrate naturally into ViewModifiers that define the application's standard styles: ```swift // TextStyleModifiers.swift // Modifier for page titles struct PageTitleModifier: ViewModifier { func body(content: Content) -> some View { content .font(Typography.displayMedium) .foregroundStyle(Color.primary) } } // Modifier for section titles struct SectionTitleModifier: ViewModifier { func body(content: Content) -> some View { content .font(Typography.titleLarge) .foregroundStyle(Color.primary) } } // Modifier for secondary text struct SecondaryTextModifier: ViewModifier { func body(content: Content) -> some View { content .font(Typography.bodyMedium) .foregroundStyle(.secondary) } } // Fluent extensions for all text styles extension View { func pageTitle() -> some View { modifier(PageTitleModifier()) } func sectionTitle() -> some View { modifier(SectionTitleModifier()) } func secondaryText() -> some View { modifier(SecondaryTextModifier()) } } ``` Usage becomes expressive and self-documenting, making code readable even for new team members. ## Interactive Modifiers with State ViewModifiers can manage their own state to create complex interactive behaviors. This pattern proves powerful for animations and user feedback. ```swift // PressableModifier.swift struct PressableModifier: ViewModifier { // Internal modifier state @State private var isPressed = false // Configuration let scale: CGFloat let opacity: CGFloat let animation: Animation init( scale: CGFloat = 0.95, opacity: CGFloat = 0.8, animation: Animation = .easeInOut(duration: 0.1) ) { self.scale = scale self.opacity = opacity self.animation = animation } func body(content: Content) -> some View { content .scaleEffect(isPressed ? scale : 1.0) .opacity(isPressed ? opacity : 1.0) .animation(animation, value: isPressed) .simultaneousGesture( DragGesture(minimumDistance: 0) .onChanged { _ in if !isPressed { isPressed = true } } .onEnded { _ in isPressed = false } ) } } extension View { func pressable( scale: CGFloat = 0.95, opacity: CGFloat = 0.8 ) -> some View { modifier(PressableModifier(scale: scale, opacity: opacity)) } } ``` ### Loading Animation with Shimmer Effect Another example of internal state: the shimmer effect for loading placeholders: ```swift // ShimmerModifier.swift struct ShimmerModifier: ViewModifier { // Continuous animation @State private var phase: CGFloat = 0 let duration: Double let bounce: Bool init(duration: Double = 1.5, bounce: Bool = false) { self.duration = duration self.bounce = bounce } func body(content: Content) -> some View { content .overlay( shimmerOverlay .mask(content) ) .onAppear { withAnimation( .linear(duration: duration) .repeatForever(autoreverses: bounce) ) { phase = 1 } } } private var shimmerOverlay: some View { GeometryReader { geometry in LinearGradient( colors: [ .clear, .white.opacity(0.5), .clear ], startPoint: .leading, endPoint: .trailing ) .frame(width: geometry.size.width * 2) .offset(x: -geometry.size.width + (geometry.size.width * 2 * phase)) } } } extension View { func shimmer(duration: Double = 1.5) -> some View { modifier(ShimmerModifier(duration: duration)) } } // Usage for loading skeletons struct SkeletonCard: View { var body: some View { VStack(alignment: .leading, spacing: Spacing.sm) { RoundedRectangle(cornerRadius: Radius.md) .fill(Color.gray.opacity(0.3)) .frame(height: 120) RoundedRectangle(cornerRadius: Radius.sm) .fill(Color.gray.opacity(0.3)) .frame(height: 20) RoundedRectangle(cornerRadius: Radius.sm) .fill(Color.gray.opacity(0.3)) .frame(width: 150, height: 16) } .shimmer() } } ``` ## Composing ViewModifiers The true power of ViewModifiers lies in their composition. Multiple modifiers can combine to create complex styles from simple building blocks. ```swift // ComposedModifiers.swift // Base modifier for primary buttons struct PrimaryButtonStyleModifier: ViewModifier { let isEnabled: Bool func body(content: Content) -> some View { content .font(Typography.bodyLarge.weight(.semibold)) .foregroundStyle(.white) .frame(maxWidth: .infinity) .padding(.vertical, Spacing.md) .background(isEnabled ? SemanticColor.primary : Color.gray) .cornerRadius(Radius.md) } } // Modifier for secondary buttons (outline) struct SecondaryButtonStyleModifier: ViewModifier { let isEnabled: Bool func body(content: Content) -> some View { content .font(Typography.bodyLarge.weight(.semibold)) .foregroundStyle(isEnabled ? SemanticColor.primary : .gray) .frame(maxWidth: .infinity) .padding(.vertical, Spacing.md) .background(Color.clear) .overlay( RoundedRectangle(cornerRadius: Radius.md) .stroke( isEnabled ? SemanticColor.primary : Color.gray, lineWidth: 2 ) ) } } // Extension combining style and interaction extension View { func primaryButton(isEnabled: Bool = true) -> some View { self .modifier(PrimaryButtonStyleModifier(isEnabled: isEnabled)) .pressable(scale: isEnabled ? 0.98 : 1.0) .allowsHitTesting(isEnabled) } func secondaryButton(isEnabled: Bool = true) -> some View { self .modifier(SecondaryButtonStyleModifier(isEnabled: isEnabled)) .pressable(scale: isEnabled ? 0.98 : 1.0) .allowsHitTesting(isEnabled) } } ``` This composition enables creating variations without code duplication. Each modifier remains independently testable and modifiable. > **Order of modifiers matters** > > The order of modifier application impacts the visual result. `padding` before `background` creates space between content and background, while the reverse adds padding around the background. ## Adaptive Modifiers with Environment ViewModifiers can read SwiftUI environment values to adapt to context: dark mode, accessible text size, orientation, and more. ```swift // AdaptiveCardModifier.swift struct AdaptiveCardModifier: ViewModifier { // Reading environment values @Environment(\.colorScheme) private var colorScheme @Environment(\.dynamicTypeSize) private var dynamicTypeSize @Environment(\.horizontalSizeClass) private var horizontalSizeClass func body(content: Content) -> some View { content .padding(adaptivePadding) .background(backgroundColor) .cornerRadius(Radius.lg) .shadow( color: shadowColor, radius: shadowRadius, x: 0, y: shadowOffset ) } // Padding adapted to text size private var adaptivePadding: CGFloat { switch dynamicTypeSize { case .xSmall, .small, .medium: return Spacing.md case .large, .xLarge: return Spacing.lg default: return Spacing.xl } } // Background color based on theme private var backgroundColor: Color { colorScheme == .dark ? Color(.secondarySystemBackground) : Color(.systemBackground) } // More subtle shadow in dark mode private var shadowColor: Color { colorScheme == .dark ? .clear : .black.opacity(0.1) } private var shadowRadius: CGFloat { colorScheme == .dark ? 0 : 8 } private var shadowOffset: CGFloat { colorScheme == .dark ? 0 : 4 } } extension View { func adaptiveCard() -> some View { modifier(AdaptiveCardModifier()) } } ``` This modifier automatically adapts to system preferences without requiring additional code in the views that use it. ## Organizing a Complete Design System A structured design system groups ViewModifiers by functional category to facilitate discovery and maintenance: ```swift // DesignSystem.swift // MARK: - Namespace for the Design System enum DS { // MARK: Text Styles enum Text { static func title(_ content: some View) -> some View { content.modifier(PageTitleModifier()) } static func section(_ content: some View) -> some View { content.modifier(SectionTitleModifier()) } static func body(_ content: some View) -> some View { content.font(Typography.bodyLarge) } static func caption(_ content: some View) -> some View { content.modifier(SecondaryTextModifier()) } } // MARK: Container Styles enum Container { static func card(_ content: some View) -> some View { content.cardStyle() } static func adaptiveCard(_ content: some View) -> some View { content.adaptiveCard() } } // MARK: Button Styles enum Button { static func primary(_ content: some View, enabled: Bool = true) -> some View { content.primaryButton(isEnabled: enabled) } static func secondary(_ content: some View, enabled: Bool = true) -> some View { content.secondaryButton(isEnabled: enabled) } } // MARK: Effects enum Effect { static func shimmer(_ content: some View) -> some View { content.shimmer() } static func pressable(_ content: some View) -> some View { content.pressable() } } } ``` ### Using the Design System This organization produces expressive code that documents its intent: ```swift // DesignSystemUsage.swift struct ProfileScreen: View { let user: User @State private var isLoading = false var body: some View { ScrollView { VStack(spacing: Spacing.lg) { // Header with title style Text("Profile") .pageTitle() // User card UserCard(user: user) .adaptiveCard() // Settings section VStack(alignment: .leading, spacing: Spacing.md) { Text("Settings") .sectionTitle() SettingsRow(title: "Notifications") SettingsRow(title: "Privacy") SettingsRow(title: "Appearance") } .adaptiveCard() // Logout button Button("Log Out") { // Action } .secondaryButton() } .padding(Spacing.md) } } } struct UserCard: View { let user: User var body: some View { HStack(spacing: Spacing.md) { AsyncImage(url: user.avatarURL) { image in image .resizable() .aspectRatio(contentMode: .fill) } placeholder: { Circle() .fill(Color.gray.opacity(0.3)) .shimmer() } .frame(width: 60, height: 60) .clipShape(Circle()) VStack(alignment: .leading, spacing: Spacing.xs) { Text(user.name) .font(Typography.titleMedium) Text(user.email) .secondaryText() } Spacer() } } } ``` ## Testing ViewModifiers ViewModifiers can be tested independently to ensure correct behavior: ```swift // ViewModifierTests.swift import XCTest import SwiftUI final class ViewModifierTests: XCTestCase { func testCardModifierAppliesCorrectCornerRadius() { // Arrange let modifier = CardModifier(cornerRadius: 20, shadowRadius: 4) // Assert XCTAssertEqual(modifier.cornerRadius, 20) } func testBadgeModifierHidesWhenCountIsZero() { // Badge should not display if count = 0 and showZero = false let modifier = BadgeModifier(count: 0, showZero: false) // Verify behavior via snapshot tests or UI tests } func testPressableModifierInitialState() { // Initial state should be not pressed // Testable via ViewInspector or UI tests } } ``` > **ViewInspector for testing** > > The ViewInspector library allows inspecting SwiftUI view content in unit tests, facilitating ViewModifier validation. ## Conclusion ViewModifiers stand as the preferred tool for building a consistent and maintainable SwiftUI design system. Their ability to encapsulate styles, animations, and behaviors into reusable components transforms the architecture of modern iOS applications. ### SwiftUI Design System Checklist - ✅ Define design tokens (spacing, radius, typography, colors) - ✅ Create atomic ViewModifiers for each style - ✅ Use `@ViewBuilder` for conditional modifiers - ✅ Manage internal state with `@State` for interactions - ✅ Compose modifiers to create complex styles - ✅ Read environment for automatic adaptation - ✅ Organize modifiers in a clear namespace - ✅ Provide View extensions for fluent syntax - ✅ Test modifiers independently - ✅ Document usage with concrete examples --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/ios/swiftui-viewmodifiers-patterns-design-system