ViewModifier tùy chỉnh trong SwiftUI: các mẫu tái sử dụng cho Design System

Xây dựng ViewModifier tùy chỉnh trong SwiftUI cho một design system nhất quán. Các mẫu, thực hành tốt nhất và ví dụ thực tế để tạo kiểu cho view iOS hiệu quả.

ViewModifier SwiftUI để xây dựng các design system tái sử dụng trong iOS

ViewModifier là nền tảng của một design system SwiftUI vững chắc và dễ bảo trì. Bằng cách đóng gói các kiểu và hành vi vào các thành phần tái sử dụng, ViewModifier đảm bảo tính nhất quán về mặt thị giác và giảm đáng kể tình trạng trùng lặp mã. Cách tiếp cận mô-đun này thay đổi cách xây dựng các giao diện iOS hiện đại.

Bài viết này bao gồm

Bài viết này trình bày các mẫu thiết yếu để tạo ra ViewModifier tùy chỉnh hiệu quả, từ cấu trúc cơ bản đến các tổ hợp nâng cao cho một design system hoàn chỉnh.

Cấu trúc của một ViewModifier

ViewModifier là một protocol định nghĩa một phép biến đổi có thể áp dụng cho bất kỳ view nào. Khác với extension của View, ViewModifier có thể duy trì trạng thái nội bộ và nhận các tham số có thể cấu hình.

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
        ))
    }
}

Mẫu Content đại diện cho view mà modifier được áp dụng. Sự trừu tượng hóa này cho phép cùng một modifier hoạt động trên bất kỳ loại view SwiftUI nào.

Sử dụng CardModifier trong thực tế

Sau khi được tạo, việc áp dụng modifier trở nên trực quan và đọc tự nhiên trong mã:

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?
}

Sự rõ ràng của mã cải thiện đáng kể khả năng bảo trì. Việc thay đổi kiểu của tất cả các thẻ trong ứng dụng chỉ cần một thay đổi duy nhất trong ViewModifier.

Modifier điều kiện với @ViewBuilder

ViewModifier có thể bao gồm logic điều kiện để điều chỉnh hành vi theo ngữ cảnh. Thuộc tính @ViewBuilder cho phép xây dựng các view phức tạp với các nhánh.

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
        ))
    }
}

Mẫu này cho phép thêm huy hiệu thông báo vào bất kỳ phần tử giao diện nào theo cách khai báo và có thể cấu hình.

Hiệu năng của điều kiện

SwiftUI tự động tối ưu hóa các nhánh @ViewBuilder. View không được hiển thị sẽ không được render, giúp duy trì hiệu năng ngay cả với các điều kiện phức tạp.

Design System với token kiểu

Một design system trưởng thành dựa trên token: các hằng số ngữ nghĩa định nghĩa bản sắc thị giác của ứng dụng. ViewModifier đóng gói các token này một cách hoàn hảo.

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)
}

ViewModifier dựa trên token

Các token này tích hợp tự nhiên vào các ViewModifier định nghĩa các kiểu chuẩn của ứng dụng:

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())
    }
}

Việc sử dụng trở nên có sức biểu đạt và tự lập tài liệu, giúp mã dễ đọc ngay cả với các thành viên mới của nhóm.

Sẵn sàng chinh phục phỏng vấn iOS?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Modifier tương tác với trạng thái

ViewModifier có thể quản lý trạng thái riêng để tạo ra các hành vi tương tác phức tạp. Mẫu này tỏ ra mạnh mẽ cho hoạt ảnh và phản hồi người dùng.

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))
    }
}

Hoạt ảnh tải với hiệu ứng shimmer

Một ví dụ khác về trạng thái nội bộ: hiệu ứng shimmer cho các placeholder tải:

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()
    }
}

Tổ hợp ViewModifier

Sức mạnh thực sự của ViewModifier nằm ở khả năng tổ hợp. Nhiều modifier có thể kết hợp để tạo ra các kiểu phức tạp từ những khối đơn giản.

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)
    }
}

Sự tổ hợp này cho phép tạo ra các biến thể mà không trùng lặp mã. Mỗi modifier vẫn có thể kiểm thử và sửa đổi độc lập.

Thứ tự các modifier rất quan trọng

Thứ tự áp dụng các modifier ảnh hưởng đến kết quả thị giác. padding trước background tạo khoảng cách giữa nội dung và nền, trong khi thứ tự ngược lại thêm padding xung quanh nền.

Modifier thích ứng với Environment

ViewModifier có thể đọc các giá trị môi trường SwiftUI để thích ứng với ngữ cảnh: chế độ tối, kích thước văn bản dễ tiếp cận, hướng và nhiều hơn nữa.

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())
    }
}

Modifier này tự động thích ứng với các tùy chọn hệ thống mà không yêu cầu mã bổ sung trong các view sử dụng nó.

Tổ chức một Design System hoàn chỉnh

Một design system có cấu trúc nhóm các ViewModifier theo danh mục chức năng để dễ tìm và bảo trì:

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()
        }
    }
}

Sử dụng Design System

Cách tổ chức này tạo ra mã có sức biểu đạt và tự lập tài liệu về ý định:

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()
        }
    }
}

Kiểm thử ViewModifier

ViewModifier có thể được kiểm thử độc lập để đảm bảo hành vi đúng đắn:

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
    }
}
ViewInspector cho việc kiểm thử

Thư viện ViewInspector cho phép kiểm tra nội dung view SwiftUI trong các bài kiểm thử đơn vị, giúp xác thực ViewModifier dễ dàng hơn.

Kết luận

ViewModifier khẳng định vị trí là công cụ hàng đầu để xây dựng một design system SwiftUI nhất quán và dễ bảo trì. Khả năng đóng gói các kiểu, hoạt ảnh và hành vi vào các thành phần tái sử dụng của chúng làm thay đổi kiến trúc của các ứng dụng iOS hiện đại.

Danh sách kiểm tra Design System SwiftUI

  • ✅ Định nghĩa các token thiết kế (spacing, radius, typography, colors)
  • ✅ Tạo các ViewModifier nguyên tử cho từng kiểu
  • ✅ Sử dụng @ViewBuilder cho các modifier điều kiện
  • ✅ Quản lý trạng thái nội bộ với @State cho các tương tác
  • ✅ Tổ hợp các modifier để tạo ra các kiểu phức tạp
  • ✅ Đọc environment để thích ứng tự động
  • ✅ Tổ chức các modifier trong namespace rõ ràng
  • ✅ Cung cấp các extension View cho cú pháp trôi chảy
  • ✅ Kiểm thử các modifier độc lập
  • ✅ Lập tài liệu sử dụng với các ví dụ cụ thể

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Thẻ

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

Chia sẻ

Bài viết liên quan