Ö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.

iOS'ta yeniden kullanılabilir design system'ler oluşturmak için SwiftUI ViewModifier'lar

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 makalenin kapsamı

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.

CardModifier.swiftswift
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:

CardUsageExample.swiftswift
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.

ConditionalBadgeModifier.swiftswift
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.

Koşulluların performansı

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.

DesignTokens.swiftswift
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:

TextStyleModifiers.swiftswift
// 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.

PressableModifier.swiftswift
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:

ShimmerModifier.swiftswift
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.

ComposedModifiers.swiftswift
// 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 sırası önemlidir

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ı.

AdaptiveCardModifier.swiftswift
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:

DesignSystem.swiftswift
// 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:

DesignSystemUsage.swiftswift
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:

ViewModifierTests.swiftswift
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
    }
}
Testler için ViewInspector

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 @ViewBuilder kullanın
  • ✅ Etkileşimler için @State ile 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

#swiftui
#ios
#viewmodifier
#design-system
#swift

Paylaş

İlgili makaleler