# ViewModifier kustom di SwiftUI: pola yang dapat digunakan kembali untuk Design System > Bangun ViewModifier kustom di SwiftUI untuk design system yang konsisten. Pola, praktik terbaik, dan contoh praktis untuk menstilisasi view iOS secara efisien. - Published: 2026-03-01 - Updated: 2026-04-29 - Author: SharpSkill - Tags: swiftui, ios, viewmodifier, design-system, swift - Reading time: 12 min --- ViewModifier merupakan fondasi dari design system SwiftUI yang kokoh dan mudah dirawat. Dengan mengenkapsulasi gaya dan perilaku ke dalam komponen yang dapat digunakan kembali, ViewModifier menjamin konsistensi visual sekaligus mengurangi duplikasi kode secara drastis. Pendekatan modular ini mengubah cara antarmuka iOS modern dibangun. > **Yang dibahas artikel ini** > > Artikel ini menyajikan pola-pola penting untuk membuat ViewModifier kustom yang efektif, mulai dari struktur dasar hingga komposisi lanjutan untuk design system yang lengkap. ## Anatomi ViewModifier ViewModifier adalah protokol yang mendefinisikan transformasi yang dapat diterapkan ke view apa pun. Berbeda dengan ekstensi View, ViewModifier dapat memelihara state internal dan menerima parameter yang dapat dikonfigurasi. ```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 )) } } ``` Pola `Content` mewakili view tempat modifier diterapkan. Abstraksi ini memungkinkan modifier yang sama bekerja pada jenis view SwiftUI mana pun. ### Menggunakan CardModifier dalam praktik Setelah dibuat, penerapan modifier menjadi intuitif dan terbaca alami dalam kode: ```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? } ``` Kejelasan kode meningkatkan kemudahan pemeliharaan secara signifikan. Mengubah gaya seluruh kartu di aplikasi hanya memerlukan satu perubahan di ViewModifier. ## Modifier kondisional dengan @ViewBuilder ViewModifier dapat menyertakan logika kondisional untuk menyesuaikan perilakunya dengan konteks. Atribut `@ViewBuilder` memungkinkan pembuatan view kompleks dengan percabangan. ```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 )) } } ``` Pola ini memungkinkan penambahan badge notifikasi pada elemen antarmuka mana pun secara deklaratif dan dapat dikonfigurasi. > **Performa kondisional** > > SwiftUI mengoptimalkan cabang `@ViewBuilder` secara otomatis. View yang tidak ditampilkan tidak dirender, sehingga performa tetap terjaga bahkan dengan kondisi kompleks. ## Design System dengan token gaya Design system yang matang bertumpu pada token: konstanta semantik yang mendefinisikan identitas visual aplikasi. ViewModifier mengenkapsulasi token-token ini secara sempurna. ```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) } ``` ### ViewModifier berbasis token Token-token ini terintegrasi secara alami dalam ViewModifier yang mendefinisikan gaya standar aplikasi: ```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()) } } ``` Penggunaan menjadi ekspresif dan mendokumentasikan dirinya sendiri, sehingga kode tetap terbaca bahkan oleh anggota tim baru. ## Modifier interaktif dengan state ViewModifier dapat mengelola state-nya sendiri untuk menciptakan perilaku interaktif yang kompleks. Pola ini terbukti ampuh untuk animasi dan umpan balik kepada pengguna. ```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)) } } ``` ### Animasi pemuatan dengan efek shimmer Contoh lain dari state internal: efek shimmer untuk placeholder pemuatan: ```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() } } ``` ## Komposisi ViewModifier Kekuatan sejati ViewModifier terletak pada komposisinya. Beberapa modifier dapat digabungkan untuk menciptakan gaya kompleks dari blok-blok sederhana. ```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) } } ``` Komposisi ini memungkinkan pembuatan variasi tanpa duplikasi kode. Setiap modifier tetap dapat diuji dan dimodifikasi secara independen. > **Urutan modifier penting** > > Urutan penerapan modifier memengaruhi hasil visual. `padding` sebelum `background` menciptakan ruang antara konten dan latar belakang, sedangkan urutan sebaliknya menambahkan padding di sekitar latar belakang. ## Modifier adaptif dengan Environment ViewModifier dapat membaca nilai environment SwiftUI untuk menyesuaikan diri dengan konteks: mode gelap, ukuran teks aksesibel, orientasi, dan lainnya. ```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()) } } ``` Modifier ini menyesuaikan diri secara otomatis dengan preferensi sistem tanpa memerlukan kode tambahan di view yang menggunakannya. ## Mengorganisasikan Design System lengkap Design system yang terstruktur mengelompokkan ViewModifier berdasarkan kategori fungsional untuk mempermudah penemuan dan pemeliharaan: ```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() } } } ``` ### Menggunakan Design System Organisasi ini menghasilkan kode ekspresif yang mendokumentasikan niatnya sendiri: ```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() } } } ``` ## Pengujian ViewModifier ViewModifier dapat diuji secara independen untuk memastikan perilaku yang benar: ```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 untuk pengujian** > > Pustaka ViewInspector memungkinkan inspeksi konten view SwiftUI dalam pengujian unit, mempermudah validasi ViewModifier. ## Kesimpulan ViewModifier menjadi pilihan utama untuk membangun design system SwiftUI yang konsisten dan mudah dirawat. Kemampuannya untuk mengenkapsulasi gaya, animasi, dan perilaku dalam komponen yang dapat digunakan kembali mengubah arsitektur aplikasi iOS modern. ### Daftar periksa Design System SwiftUI - ✅ Mendefinisikan token desain (spacing, radius, typography, colors) - ✅ Membuat ViewModifier atomik untuk setiap gaya - ✅ Menggunakan `@ViewBuilder` untuk modifier kondisional - ✅ Mengelola state internal dengan `@State` untuk interaksi - ✅ Menyusun modifier untuk menciptakan gaya kompleks - ✅ Membaca environment untuk adaptasi otomatis - ✅ Mengorganisasikan modifier dalam namespace yang jelas - ✅ Menyediakan ekstensi View untuk sintaks yang fasih - ✅ Menguji modifier secara independen - ✅ Mendokumentasikan penggunaan dengan contoh konkret --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/ios/swiftui-viewmodifiers-patterns-design-system