Özel SwiftUI ViewModifier'lar: Design System için yeniden kullanılabilir desenler
Tutarlı bir design system için SwiftUI'da özel ViewModifier'lar oluşturun. iOS view'lerini verimli şekilde stillendirmek için desenler, en iyi uygulamalar ve pratik örnekler.

ViewModifier'lar, sağlam ve sürdürülebilir bir SwiftUI design system'in temelini oluşturur. Stilleri ve davranışları yeniden kullanılabilir bileşenlerde kapsülleyerek görsel tutarlılık sağlar ve kod tekrarını ciddi ölçüde azaltır. Bu modüler yaklaşım, modern iOS arayüzlerinin oluşturulma şeklini dönüştürür.
Bu makale, temel yapıdan tam bir design system için gelişmiş kompozisyonlara kadar etkili özel ViewModifier'lar oluşturmaya yönelik temel desenleri sunar.
ViewModifier'ın anatomisi
ViewModifier, herhangi bir view'a uygulanabilen bir dönüşümü tanımlayan bir protokoldür. View extension'larından farklı olarak, ViewModifier'lar dahili durum tutabilir ve yapılandırılabilir parametreler kabul edebilir.
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
))
}
}Content deseni, modifier'ın uygulandığı view'ı temsil eder. Bu soyutlama, aynı modifier'ın herhangi bir SwiftUI view tipi üzerinde çalışmasını sağlar.
CardModifier'ı pratikte kullanmak
Oluşturulduktan sonra modifier'ı uygulamak sezgisel hale gelir ve kodda doğal şekilde okunur:
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?
}Kodun netliği bakım kolaylığını belirgin şekilde artırır. Uygulamadaki tüm kart'ların stilini değiştirmek yalnızca ViewModifier'da tek bir değişiklik gerektirir.
@ViewBuilder ile koşullu modifier'lar
ViewModifier'lar, davranışlarını bağlama göre uyarlamak için koşullu mantık içerebilir. @ViewBuilder özelliği, dallanmalarla karmaşık view'lar oluşturmaya olanak tanır.
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
))
}
}Bu desen, herhangi bir arayüz öğesine bildirimli ve yapılandırılabilir şekilde bildirim badge'leri eklemeyi mümkün kılar.
SwiftUI, @ViewBuilder dallarını otomatik olarak optimize eder. Görüntülenmeyen view render edilmez ve karmaşık koşullarda bile performans korunur.
Stil token'ları ile Design System
Olgun bir design system, token'lara dayanır: uygulamanın görsel kimliğini tanımlayan semantik sabitler. ViewModifier'lar bu token'ları mükemmel şekilde kapsüller.
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 tabanlı ViewModifier'lar
Bu token'lar, uygulamanın standart stillerini tanımlayan ViewModifier'larla doğal olarak entegre olur:
// 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())
}
}Kullanım ifade gücü kazanır ve kendini belgelendirir, böylece kod yeni ekip üyeleri için bile okunabilir kalır.
iOS mülakatlarında başarılı olmaya hazır mısın?
İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.
Durumlu etkileşimli modifier'lar
ViewModifier'lar, karmaşık etkileşimli davranışlar oluşturmak için kendi durumlarını yönetebilir. Bu desen, animasyonlar ve kullanıcı geri bildirimi için güçlüdür.
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))
}
}Shimmer efekti ile yükleme animasyonu
Dahili durum için başka bir örnek: yükleme placeholder'ları için shimmer efekti:
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()
}
}ViewModifier'ların kompozisyonu
ViewModifier'ların gerçek gücü, kompozisyonlarındadır. Birden fazla modifier birleştirilerek basit yapı taşlarından karmaşık stiller oluşturulabilir.
// 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)
}
}Bu kompozisyon, kod tekrarı olmadan varyasyonlar oluşturmayı sağlar. Her modifier bağımsız olarak test edilebilir ve değiştirilebilir kalır.
Modifier'ların uygulanma sırası görsel sonucu etkiler. padding, background'tan önce gelirse içerik ile arka plan arasında boşluk oluşur; ters sıra ise arka planın etrafına padding ekler.
Environment ile uyarlanabilir modifier'lar
ViewModifier'lar, bağlama uyum sağlamak için SwiftUI environment değerlerini okuyabilir: koyu mod, erişilebilir metin boyutu, yön ve daha fazlası.
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())
}
}Bu modifier, kullanan view'larda ek kod gerektirmeden sistem tercihlerine otomatik olarak uyum sağlar.
Tam bir Design System'i organize etmek
Yapılandırılmış bir design system, keşif ve bakım kolaylığı için ViewModifier'ları işlevsel kategorilere göre gruplar:
// 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()
}
}
}Design System'i kullanmak
Bu organizasyon, kendi niyetini belgeleyen ifade gücü yüksek bir kod üretir:
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'ları test etmek
ViewModifier'lar, doğru davranışı garanti etmek için bağımsız olarak test edilebilir:
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 kütüphanesi, birim testlerinde SwiftUI view içeriğini incelemeyi sağlar ve ViewModifier'ların doğrulanmasını kolaylaştırır.
Sonuç
ViewModifier'lar, tutarlı ve sürdürülebilir bir SwiftUI design system oluşturmak için tercih edilen araç olarak öne çıkar. Stilleri, animasyonları ve davranışları yeniden kullanılabilir bileşenlerde kapsülleme yetenekleri, modern iOS uygulamalarının mimarisini dönüştürür.
SwiftUI Design System kontrol listesi
- ✅ Design token'larını tanımlayın (spacing, radius, typography, colors)
- ✅ Her stil için atomik ViewModifier'lar oluşturun
- ✅ Koşullu modifier'lar için
@ViewBuilderkullanın - ✅ Etkileşimler için
@Stateile dahili durumu yönetin - ✅ Karmaşık stiller oluşturmak için modifier'ları birleştirin
- ✅ Otomatik uyum için environment'ı okuyun
- ✅ Modifier'ları net bir namespace içinde organize edin
- ✅ Akıcı sözdizimi için View extension'ları sağlayın
- ✅ Modifier'ları bağımsız olarak test edin
- ✅ Kullanımı somut örneklerle belgeleyin
Pratik yapmaya başla!
Mülakat simülatörleri ve teknik testlerle bilgini test et.
Etiketler
Paylaş
İlgili makaleler

SwiftUI Performans: LazyVStack ve Karmaşık Listeleri Optimize Etme
LazyVStack ve SwiftUI listeleri için optimizasyon teknikleri. Bellek tüketimini azaltın, kaydırma performansını iyileştirin ve sık karşılaşılan tuzakları önleyin.

SwiftUI @Observable vs @State: 2026'da Hangisini Ne Zaman Kullanmalı
iOS uygulamaları için doğru durum yönetimi aracını seçmek üzere SwiftUI'daki @Observable ve @State arasındaki farkları öğrenin.

SwiftUI: iOS icin Modern Arayuzler Olusturma
SwiftUI ile modern kullanici arayuzleri olusturma rehberi: deklaratif sozdizimi, bilesenler, animasyonlar ve iOS 18 icin en iyi uygulamalar.