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.

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.
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.
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:
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.
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.
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.
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:
// 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.
Siap menguasai wawancara iOS Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
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.
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:
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.
// 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 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.
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:
// 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:
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:
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
}
}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
@ViewBuilderuntuk modifier kondisional - ✅ Mengelola state internal dengan
@Stateuntuk 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
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Tag
Bagikan
Artikel terkait

Performa SwiftUI: Mengoptimalkan LazyVStack dan Daftar Kompleks
Teknik optimasi untuk LazyVStack dan daftar SwiftUI. Mengurangi konsumsi memori, meningkatkan performa scroll, dan menghindari kesalahan umum.

SwiftUI @Observable vs @State: Kapan Menggunakan yang Mana di 2026
Kuasai perbedaan antara @Observable dan @State di SwiftUI untuk memilih alat manajemen state yang tepat untuk aplikasi iOS Anda.

SwiftUI: Membangun Antarmuka Modern untuk iOS
Panduan lengkap membangun antarmuka modern dengan SwiftUI: sintaks deklaratif, komponen, animasi, dan praktik terbaik untuk iOS 18.