Eigene SwiftUI-ViewModifier: wiederverwendbare Patterns für Design Systems
Eigene ViewModifier in SwiftUI für ein konsistentes Design System bauen. Patterns, Best Practices und praxisnahe Beispiele für effizientes iOS-View-Styling.

ViewModifier bilden das Fundament eines robusten und wartbaren SwiftUI-Design-Systems. Indem sie Stile und Verhalten in wiederverwendbaren Komponenten kapseln, sorgen sie für visuelle Konsistenz und reduzieren Code-Duplikation drastisch. Dieser modulare Ansatz verändert grundlegend, wie moderne iOS-Oberflächen entstehen.
Dieser Artikel zeigt die wesentlichen Patterns zur Erstellung effektiver eigener ViewModifier – von der Grundstruktur bis zu fortgeschrittenen Kompositionen für ein vollständiges Design System.
Anatomie eines ViewModifiers
Ein ViewModifier ist ein Protokoll, das eine Transformation definiert, die auf jede beliebige View angewendet werden kann. Im Gegensatz zu View-Extensions können ViewModifier internen State halten und konfigurierbare Parameter entgegennehmen.
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
))
}
}Das Content-Pattern repräsentiert die View, auf die der Modifier angewendet wird. Diese Abstraktion erlaubt es, denselben Modifier auf jeden beliebigen SwiftUI-View-Typ anzuwenden.
Den CardModifier in der Praxis einsetzen
Nach der Definition wird die Anwendung intuitiv und liest sich natürlich im 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?
}Die Code-Klarheit verbessert die Wartbarkeit deutlich. Der Stil aller Cards in der App lässt sich durch eine einzige Änderung am ViewModifier anpassen.
Bedingte Modifier mit @ViewBuilder
ViewModifier können bedingte Logik enthalten, um ihr Verhalten an den Kontext anzupassen. Das Attribut @ViewBuilder erlaubt es, komplexe Views mit Verzweigungen zu bauen.
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
))
}
}Mit diesem Pattern lassen sich Notification-Badges deklarativ und konfigurierbar an jedes Interface-Element anhängen.
SwiftUI optimiert @ViewBuilder-Zweige automatisch. Die nicht angezeigte View wird nicht gerendert, sodass die Performance auch bei komplexen Bedingungen erhalten bleibt.
Design System mit Style-Tokens
Ein ausgereiftes Design System basiert auf Tokens: semantischen Konstanten, die die visuelle Identität der App definieren. ViewModifier kapseln diese Tokens nahezu perfekt.
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-basierte ViewModifier
Diese Tokens fügen sich natürlich in ViewModifier ein, die die Standard-Stile der App definieren:
// 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())
}
}Die Anwendung ist ausdrucksstark und selbstdokumentierend, was den Code auch für neue Teammitglieder lesbar macht.
Bereit für deine iOS-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
Interaktive Modifier mit State
ViewModifier können ihren eigenen State verwalten, um komplexe interaktive Verhalten zu ermöglichen. Dieses Pattern eignet sich hervorragend für Animationen und User-Feedback.
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))
}
}Lade-Animation mit Shimmer-Effekt
Ein weiteres Beispiel für internen State: der Shimmer-Effekt für Lade-Platzhalter:
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()
}
}Komposition von ViewModifiern
Die wahre Stärke der ViewModifier liegt in ihrer Komposition. Mehrere Modifier lassen sich kombinieren, um aus einfachen Bausteinen komplexe Stile zu erzeugen.
// 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)
}
}Diese Komposition ermöglicht Variationen ohne Code-Duplikation. Jeder Modifier bleibt unabhängig testbar und veränderbar.
Die Reihenfolge der Modifier-Anwendung beeinflusst das visuelle Ergebnis. padding vor background erzeugt Abstand zwischen Inhalt und Hintergrund, während die umgekehrte Reihenfolge Padding um den Hintergrund herum hinzufügt.
Adaptive Modifier mit Environment
ViewModifier können SwiftUI-Environment-Werte lesen, um sich an den Kontext anzupassen: Dark Mode, barrierefreie Schriftgröße, Ausrichtung und mehr.
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())
}
}Dieser Modifier passt sich automatisch an die System-Einstellungen an, ohne dass die nutzenden Views zusätzlichen Code benötigen.
Aufbau eines vollständigen Design Systems
Ein strukturiertes Design System gruppiert ViewModifier nach funktionaler Kategorie, um Auffindbarkeit und Wartung zu erleichtern:
// 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()
}
}
}Das Design System verwenden
Diese Organisation produziert ausdrucksstarken Code, der seine eigene Intention dokumentiert:
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()
}
}
}ViewModifier testen
ViewModifier lassen sich unabhängig testen, um korrektes Verhalten sicherzustellen:
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
}
}Die Bibliothek ViewInspector erlaubt das Inspizieren von SwiftUI-View-Inhalten in Unit-Tests und erleichtert die Validierung von ViewModifiern.
Fazit
ViewModifier setzen sich als bevorzugtes Werkzeug zum Aufbau eines konsistenten und wartbaren SwiftUI-Design-Systems durch. Ihre Fähigkeit, Stile, Animationen und Verhalten in wiederverwendbare Komponenten zu kapseln, verändert die Architektur moderner iOS-Apps grundlegend.
Checkliste für SwiftUI-Design-Systems
- ✅ Design-Tokens definieren (Spacing, Radius, Typography, Colors)
- ✅ Atomare ViewModifier für jeden Stil erstellen
- ✅
@ViewBuilderfür bedingte Modifier verwenden - ✅ Internen State mit
@Statefür Interaktionen verwalten - ✅ Modifier komponieren, um komplexe Stile zu erzeugen
- ✅ Environment auslesen für automatische Anpassung
- ✅ Modifier in einem klaren Namespace organisieren
- ✅ View-Extensions für eine flüssige Syntax bereitstellen
- ✅ Modifier unabhängig testen
- ✅ Verwendung mit konkreten Beispielen dokumentieren
Fang an zu üben!
Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.
Tags
Teilen
Verwandte Artikel

SwiftUI-Performance: LazyVStack und komplexe Listen optimieren
Optimierungstechniken für LazyVStack und SwiftUI-Listen. Speicherverbrauch reduzieren, Scroll-Performance verbessern und häufige Fallstricke vermeiden.

SwiftUI @Observable vs @State: Wann welches verwenden in 2026
Beherrsche die Unterschiede zwischen @Observable und @State in SwiftUI, um das richtige Tool für State-Management in iOS-Apps zu wählen.

SwiftUI: Moderne Interfaces fuer iOS entwickeln
Umfassende Anleitung zur Erstellung moderner Benutzeroberflaechen mit SwiftUI: deklarative Syntax, Komponenten, Animationen und Best Practices fuer iOS 18.