# Custom SwiftUI ViewModifiers: herbruikbare patterns voor design systems > Bouw custom ViewModifiers in SwiftUI voor een consistent design system. Patterns, best practices en praktische voorbeelden om iOS-views efficiënt te stijlen. - Published: 2026-03-01 - Updated: 2026-04-29 - Author: SharpSkill - Tags: swiftui, ios, viewmodifier, design-system, swift - Reading time: 12 min --- ViewModifiers vormen het fundament van een robuust en onderhoudbaar SwiftUI design system. Door stijlen en gedrag in herbruikbare componenten te kapselen, garanderen ze visuele consistentie en verminderen ze codeduplicatie drastisch. Deze modulaire aanpak verandert de manier waarop moderne iOS-interfaces worden gebouwd. > **Wat dit artikel behandelt** > > Dit artikel presenteert de essentiële patterns voor het maken van effectieve custom ViewModifiers, van basisstructuur tot geavanceerde composities voor een volledig design system. ## Anatomie van een ViewModifier Een ViewModifier is een protocol dat een transformatie definieert die op elke view kan worden toegepast. In tegenstelling tot View-extensions kunnen ViewModifiers interne state behouden en configureerbare parameters accepteren. ```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 )) } } ``` Het `Content`-pattern vertegenwoordigt de view waarop de modifier wordt toegepast. Deze abstractie zorgt ervoor dat dezelfde modifier op elk type SwiftUI-view werkt. ### De CardModifier in de praktijk gebruiken Na het aanmaken wordt het toepassen van de modifier intuïtief en leest het natuurlijk in de 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? } ``` De duidelijkheid van de code verbetert de onderhoudbaarheid aanzienlijk. De stijl van alle cards in de app aanpassen vereist slechts één wijziging in de ViewModifier. ## Conditionele modifiers met @ViewBuilder ViewModifiers kunnen conditionele logica bevatten om het gedrag aan de context aan te passen. Het attribuut `@ViewBuilder` maakt het mogelijk om complexe views met vertakkingen te bouwen. ```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 )) } } ``` Dit pattern maakt het mogelijk om notificatiebadges declaratief en configureerbaar aan elk interface-element toe te voegen. > **Performance van conditionals** > > SwiftUI optimaliseert `@ViewBuilder`-takken automatisch. De niet-getoonde view wordt niet gerenderd, waardoor de performance behouden blijft, zelfs bij complexe condities. ## Design system met style tokens Een volwassen design system steunt op tokens: semantische constanten die de visuele identiteit van de app definiëren. ViewModifiers kapselen deze tokens perfect. ```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-gebaseerde ViewModifiers Deze tokens integreren natuurlijk in ViewModifiers die de standaardstijlen van de app definiëren: ```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()) } } ``` Het gebruik wordt expressief en zelfdocumenterend, waardoor de code leesbaar blijft, zelfs voor nieuwe teamleden. ## Interactieve modifiers met state ViewModifiers kunnen hun eigen state beheren om complex interactief gedrag te creëren. Dit pattern blijkt krachtig voor animaties en gebruikersfeedback. ```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)) } } ``` ### Laad-animatie met shimmer-effect Nog een voorbeeld van interne state: het shimmer-effect voor laad-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() } } ``` ## Compositie van ViewModifiers De echte kracht van ViewModifiers ligt in hun compositie. Meerdere modifiers kunnen gecombineerd worden om complexe stijlen te bouwen vanuit eenvoudige bouwstenen. ```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) } } ``` Deze compositie maakt variaties mogelijk zonder code te dupliceren. Elke modifier blijft onafhankelijk testbaar en aanpasbaar. > **De volgorde van modifiers telt** > > De volgorde waarin modifiers worden toegepast beïnvloedt het visuele resultaat. `padding` vóór `background` creëert ruimte tussen content en achtergrond, terwijl de omgekeerde volgorde padding rond de achtergrond toevoegt. ## Adaptieve modifiers met Environment ViewModifiers kunnen SwiftUI environment-waarden lezen om zich aan te passen aan de context: dark mode, toegankelijke tekstgrootte, oriëntatie en meer. ```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()) } } ``` Deze modifier past zich automatisch aan aan systeemvoorkeuren zonder extra code in de gebruikende views te vereisen. ## Een volledig design system organiseren Een gestructureerd design system groepeert ViewModifiers per functionele categorie om vindbaarheid en onderhoud te vergemakkelijken: ```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() } } } ``` ### Het design system gebruiken Deze organisatie produceert expressieve code die zijn eigen intentie documenteert: ```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() } } } ``` ## ViewModifiers testen ViewModifiers kunnen onafhankelijk getest worden om correct gedrag te garanderen: ```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 voor tests** > > De ViewInspector-bibliotheek maakt het mogelijk om de inhoud van SwiftUI-views te inspecteren in unit tests, wat de validatie van ViewModifiers vergemakkelijkt. ## Conclusie ViewModifiers zijn dé tool om een consistent en onderhoudbaar SwiftUI design system te bouwen. Hun vermogen om stijlen, animaties en gedrag in herbruikbare componenten te kapselen, transformeert de architectuur van moderne iOS-apps. ### SwiftUI design system checklist - ✅ Definieer design tokens (spacing, radius, typography, colors) - ✅ Maak atomaire ViewModifiers voor elke stijl - ✅ Gebruik `@ViewBuilder` voor conditionele modifiers - ✅ Beheer interne state met `@State` voor interacties - ✅ Componeer modifiers om complexe stijlen te creëren - ✅ Lees environment uit voor automatische adaptatie - ✅ Organiseer modifiers in een duidelijke namespace - ✅ Bied View-extensions voor een vloeiende syntax - ✅ Test modifiers onafhankelijk - ✅ Documenteer het gebruik met concrete voorbeelden --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/ios/swiftui-viewmodifiers-patterns-design-system