AgentSkillsCN

SwiftUI Patterns

面向 iOS 18+ 的现代 SwiftUI 设计模式,包括 @Observable、导航、状态管理以及遵循 SOLID 原则的视图组合

SKILL.md
--- frontmatter
name: SwiftUI Patterns
description: Modern SwiftUI patterns for iOS 18+, including @Observable, navigation, state management, and view composition following SOLID principles
version: 1.0.0

SwiftUI Patterns Skill

Master modern SwiftUI development with Swift 6 features, @Observable macro, NavigationStack, and architectural patterns that follow SOLID principles.

State Management (iOS 18+ / Swift 6)

@Observable Macro (Preferred over ObservableObject)

The @Observable macro is the modern way to create observable state in SwiftUI:

swift
import SwiftUI

@Observable
final class UserViewModel {
    var user: User?
    var isLoading = false
    var errorMessage: String?

    private let repository: UserRepositoryProtocol

    init(repository: UserRepositoryProtocol) {
        self.repository = repository
    }

    @MainActor
    func loadUser(id: String) async {
        isLoading = true
        defer { isLoading = false }

        do {
            user = try await repository.fetch(id: id)
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}

State Property Wrappers

WrapperUse CaseScope
@StateView-local primitive stateSingle view
@BindingTwo-way connection to parent stateParent-child
@BindableTwo-way binding to @Observable propertyWith @Observable
@EnvironmentDependency injectionEnvironment chain
@ObservableShared mutable state (replaces @ObservableObject)Across views

Using @Bindable with @Observable

swift
@Observable
final class FormViewModel {
    var email = ""
    var password = ""
    var isValid: Bool {
        !email.isEmpty && password.count >= 8
    }
}

struct LoginForm: View {
    @Bindable var viewModel: FormViewModel

    var body: some View {
        Form {
            TextField("Email", text: $viewModel.email)
            SecureField("Password", text: $viewModel.password)
            Button("Login") { /* ... */ }
                .disabled(!viewModel.isValid)
        }
    }
}

Navigation (NavigationStack)

Type-Safe Navigation with Enums

swift
enum AppRoute: Hashable {
    case userProfile(User.ID)
    case settings
    case orderDetail(Order)
    case checkout(Cart)
}

struct ContentView: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            HomeView(path: $path)
                .navigationDestination(for: AppRoute.self) { route in
                    destinationView(for: route)
                }
        }
    }

    @ViewBuilder
    private func destinationView(for route: AppRoute) -> some View {
        switch route {
        case .userProfile(let userId):
            UserProfileView(userId: userId)
        case .settings:
            SettingsView()
        case .orderDetail(let order):
            OrderDetailView(order: order)
        case .checkout(let cart):
            CheckoutView(cart: cart)
        }
    }
}

Programmatic Navigation

swift
struct ProductListView: View {
    @Binding var path: NavigationPath
    let products: [Product]

    var body: some View {
        List(products) { product in
            Button {
                path.append(AppRoute.productDetail(product.id))
            } label: {
                ProductRow(product: product)
            }
        }
    }

    func navigateToCheckout(cart: Cart) {
        path.append(AppRoute.checkout(cart))
    }

    func popToRoot() {
        path.removeLast(path.count)
    }
}

View Composition

Extract Subviews (Single Responsibility)

swift
// BAD: Monolithic view
struct OrderView: View {
    let order: Order

    var body: some View {
        VStack {
            // 100+ lines of UI code
        }
    }
}

// GOOD: Composed from smaller views
struct OrderView: View {
    let order: Order

    var body: some View {
        ScrollView {
            VStack(spacing: 16) {
                OrderHeaderView(order: order)
                OrderItemsSection(items: order.items)
                OrderSummaryView(
                    subtotal: order.subtotal,
                    tax: order.tax,
                    total: order.total
                )
                OrderActionsView(order: order)
            }
            .padding()
        }
    }
}

Reusable Components with ViewBuilder

swift
struct Card<Content: View>: View {
    let title: String?
    @ViewBuilder let content: () -> Content

    init(title: String? = nil, @ViewBuilder content: @escaping () -> Content) {
        self.title = title
        self.content = content
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            if let title {
                Text(title)
                    .font(.headline)
            }
            content()
        }
        .padding()
        .background(.regularMaterial)
        .clipShape(RoundedRectangle(cornerRadius: 12))
    }
}

// Usage
Card(title: "Order Summary") {
    LabeledContent("Subtotal", value: order.subtotal.formatted(.currency(code: "USD")))
    LabeledContent("Tax", value: order.tax.formatted(.currency(code: "USD")))
    Divider()
    LabeledContent("Total", value: order.total.formatted(.currency(code: "USD")))
        .bold()
}

ViewModifiers for Reusable Styling

swift
struct CardModifier: ViewModifier {
    let cornerRadius: CGFloat

    func body(content: Content) -> some View {
        content
            .padding()
            .background(.regularMaterial)
            .clipShape(RoundedRectangle(cornerRadius: cornerRadius))
            .shadow(radius: 2)
    }
}

extension View {
    func cardStyle(cornerRadius: CGFloat = 12) -> some View {
        modifier(CardModifier(cornerRadius: cornerRadius))
    }
}

// Usage
Text("Hello")
    .cardStyle()

SOLID Principles in SwiftUI

Single Responsibility Principle

Each view should have one reason to change:

swift
// View: Only presentation
struct UserListView: View {
    @State private var viewModel: UserListViewModel

    var body: some View {
        List(viewModel.users) { user in
            UserRow(user: user)
        }
        .task { await viewModel.loadUsers() }
    }
}

// ViewModel: Business logic and state
@Observable @MainActor
final class UserListViewModel {
    private let service: UserServiceProtocol
    var users: [User] = []

    init(service: UserServiceProtocol) {
        self.service = service
    }

    func loadUsers() async {
        users = (try? await service.fetchAll()) ?? []
    }
}

Open/Closed Principle

Extend behavior without modifying existing code:

swift
// Base button style
struct PrimaryButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .padding()
            .background(Color.accentColor)
            .foregroundStyle(.white)
            .clipShape(RoundedRectangle(cornerRadius: 8))
            .opacity(configuration.isPressed ? 0.8 : 1)
    }
}

// Extended without modifying original
struct LoadingButtonStyle: ButtonStyle {
    let isLoading: Bool

    func makeBody(configuration: Configuration) -> some View {
        HStack {
            if isLoading {
                ProgressView()
                    .tint(.white)
            }
            configuration.label
        }
        .padding()
        .background(Color.accentColor)
        .foregroundStyle(.white)
        .clipShape(RoundedRectangle(cornerRadius: 8))
        .opacity(configuration.isPressed || isLoading ? 0.8 : 1)
    }
}

Dependency Inversion Principle

Depend on abstractions via Environment:

swift
// Protocol abstraction
protocol AnalyticsProtocol {
    func track(event: String, properties: [String: Any])
}

// Environment key
private struct AnalyticsKey: EnvironmentKey {
    static let defaultValue: AnalyticsProtocol = NoOpAnalytics()
}

extension EnvironmentValues {
    var analytics: AnalyticsProtocol {
        get { self[AnalyticsKey.self] }
        set { self[AnalyticsKey.self] = newValue }
    }
}

// Usage in view
struct ProductView: View {
    @Environment(\.analytics) private var analytics
    let product: Product

    var body: some View {
        Button("Buy") {
            analytics.track(event: "purchase", properties: ["product_id": product.id])
        }
    }
}

// Inject real or mock
ProductView(product: product)
    .environment(\.analytics, FirebaseAnalytics()) // Production
    .environment(\.analytics, MockAnalytics())     // Testing

Performance Optimization

Avoid Expensive Body Computations

swift
// BAD: Expensive computation in body
var body: some View {
    let sortedItems = items.sorted { $0.date > $1.date } // Called on every render
    List(sortedItems) { item in
        ItemRow(item: item)
    }
}

// GOOD: Computed outside body or cached
@Observable
final class ItemListViewModel {
    var items: [Item] = []

    var sortedItems: [Item] {
        items.sorted { $0.date > $1.date }
    }
}

Use @Previewable for Previews (iOS 18+)

swift
#Preview {
    @Previewable @State var count = 0

    VStack {
        Text("Count: \(count)")
        Button("+1") { count += 1 }
    }
}

Lazy Loading

swift
LazyVStack {
    ForEach(items) { item in
        ItemRow(item: item)
    }
}

LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))]) {
    ForEach(products) { product in
        ProductCard(product: product)
    }
}

References

  • See references/observable-migration.md for ObservableObject to @Observable migration guide
  • See references/navigation-patterns.md for advanced navigation patterns
  • See references/mvvm-architecture.md for complete MVVM implementation