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.

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.
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.
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:
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.
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.
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.
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:
// 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.
Klaar om je iOS gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
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.
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:
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.
// 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 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.
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:
// 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:
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:
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
}
}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
@ViewBuildervoor conditionele modifiers - ✅ Beheer interne state met
@Statevoor 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
Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
Tags
Delen
Gerelateerde artikelen

SwiftUI Performance: LazyVStack en Complexe Lijsten Optimaliseren
Optimalisatietechnieken voor LazyVStack en SwiftUI-lijsten. Verminder geheugengebruik, verbeter scrollprestaties en vermijd veelvoorkomende valkuilen.

SwiftUI @Observable vs @State: Wanneer Wat Gebruiken in 2026
Beheers de verschillen tussen @Observable en @State in SwiftUI om de juiste tool voor state management in iOS-apps te kiezen.

SwiftUI: Moderne Interfaces Bouwen voor iOS
Een complete handleiding voor het bouwen van moderne gebruikersinterfaces met SwiftUI: declaratieve syntaxis, componenten, animaties en best practices voor iOS 18.