swiftui-patterns

swiftui-patterns

熱門

SwiftUI 架構模式、使用 @Observable 的狀態管理、視圖組合、導航、效能最佳化,以及現代 iOS/macOS UI 最佳實務。

23萬星標
3.5萬分支
更新於 2026/7/14
SKILL.md
readonlyread-only
name
swiftui-patterns
description

SwiftUI 架構模式、使用 @Observable 的狀態管理、視圖組合、導航、效能最佳化,以及現代 iOS/macOS UI 最佳實務。

SwiftUI 模式

現代 SwiftUI 模式,用於在 Apple 平台上建立宣告式、高效能的使用者介面。涵蓋 Observation 框架、視圖組合、型別安全導航以及效能最佳化。

何時啟用

  • 建立 SwiftUI 視圖並管理狀態(@State@Observable@Binding
  • 使用 NavigationStack 設計導航流程
  • 建構視圖模型與資料流
  • 最佳化列表與複雜佈局的渲染效能
  • 在 SwiftUI 中處理環境值與依賴注入

狀態管理

屬性包裝器選擇

選擇最簡單且適用的包裝器:

包裝器 使用情境
@State 視圖區域的數值型別(切換開關、表單欄位、Sheet 顯示)
@Binding 對父層 @State 的雙向參考
@Observable class + @State 擁有多個屬性的自有模型
@Observable class(無包裝器) 從父層傳入的唯讀參考
@Bindable @Observable 屬性的雙向綁定
@Environment 透過 .environment() 注入的共享依賴

@Observable ViewModel

使用 @Observable(而非 ObservableObject)— 它會追蹤屬性層級的變更,讓 SwiftUI 僅重新渲染讀取該屬性的視圖:

@Observable
final class ItemListViewModel {
    private(set) var items: [Item] = []
    private(set) var isLoading = false
    var searchText = ""

    private let repository: any ItemRepository

    init(repository: any ItemRepository = DefaultItemRepository()) {
        self.repository = repository
    }

    func load() async {
        isLoading = true
        defer { isLoading = false }
        items = (try? await repository.fetchAll()) ?? []
    }
}

使用 ViewModel 的視圖

struct ItemListView: View {
    @State private var viewModel: ItemListViewModel

    init(viewModel: ItemListViewModel = ItemListViewModel()) {
        _viewModel = State(initialValue: viewModel)
    }

    var body: some View {
        List(viewModel.items) { item in
            ItemRow(item: item)
        }
        .searchable(text: $viewModel.searchText)
        .overlay { if viewModel.isLoading { ProgressView() } }
        .task { await viewModel.load() }
    }
}

環境注入

@EnvironmentObject 替換為 @Environment

// 注入
ContentView()
    .environment(authManager)

// 使用
struct ProfileView: View {
    @Environment(AuthManager.self) private var auth

    var body: some View {
        Text(auth.currentUser?.name ?? "訪客")
    }
}

視圖組合

提取子視圖以限制重新渲染

將視圖拆分為小型、專注的結構體。當狀態變更時,僅有讀取該狀態的子視圖會重新渲染:

struct OrderView: View {
    @State private var viewModel = OrderViewModel()

    var body: some View {
        VStack {
            OrderHeader(title: viewModel.title)
            OrderItemList(items: viewModel.items)
            OrderTotal(total: viewModel.total)
        }
    }
}

使用 ViewModifier 進行可重複使用的樣式

struct CardModifier: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(.regularMaterial)
            .clipShape(RoundedRectangle(cornerRadius: 12))
    }
}

extension View {
    func cardStyle() -> some View {
        modifier(CardModifier())
    }
}

導航

型別安全的 NavigationStack

使用 NavigationStack 搭配 NavigationPath 進行程式化、型別安全的路由:

@Observable
final class Router {
    var path = NavigationPath()

    func navigate(to destination: Destination) {
        path.append(destination)
    }

    func popToRoot() {
        path = NavigationPath()
    }
}

enum Destination: Hashable {
    case detail(Item.ID)
    case settings
    case profile(User.ID)
}

struct RootView: View {
    @State private var router = Router()

    var body: some View {
        NavigationStack(path: $router.path) {
            HomeView()
                .navigationDestination(for: Destination.self) { dest in
                    switch dest {
                    case .detail(let id): ItemDetailView(itemID: id)
                    case .settings: SettingsView()
                    case .profile(let id): ProfileView(userID: id)
                    }
                }
        }
        .environment(router)
    }
}

效能

對大型集合使用 Lazy 容器

LazyVStackLazyHStack 僅在可見時才建立視圖:

ScrollView {
    LazyVStack(spacing: 8) {
        ForEach(items) { item in
            ItemRow(item: item)
        }
    }
}

穩定的識別碼

ForEach 中始終使用穩定、唯一的 ID — 避免使用陣列索引:

// 使用 Identifiable 協定或明確的 id
ForEach(items, id: \.stableID) { item in
    ItemRow(item: item)
}

避免在 body 中進行昂貴操作

  • 絕不在 body 內執行 I/O、網路呼叫或大量計算
  • 使用 .task {} 處理非同步工作 — 當視圖消失時會自動取消
  • 在滾動視圖中謹慎使用 .sensoryFeedback().geometryGroup()
  • 盡量減少列表中的 .shadow().blur().mask() — 它們會觸發離屏渲染

Equatable 協定

對於 body 較昂貴的視圖,遵循 Equatable 以跳過不必要的重新渲染:

struct ExpensiveChartView: View, Equatable {
    let dataPoints: [DataPoint] // DataPoint 必須遵循 Equatable

    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.dataPoints == rhs.dataPoints
    }

    var body: some View {
        // 複雜圖表渲染
    }
}

預覽

使用 #Preview 巨集搭配內聯模擬資料以快速迭代:

#Preview("空狀態") {
    ItemListView(viewModel: ItemListViewModel(repository: EmptyMockRepository()))
}

#Preview("已載入") {
    ItemListView(viewModel: ItemListViewModel(repository: PopulatedMockRepository()))
}

應避免的反模式

  • 在新程式碼中使用 ObservableObject / @Published / @StateObject / @EnvironmentObject — 請遷移至 @Observable
  • 將非同步工作直接放在 bodyinit 中 — 請使用 .task {} 或明確的載入方法
  • 在不擁有資料的子視圖中將 view model 建立為 @State — 應從父層傳入
  • 使用 AnyView 型別抹消 — 對於條件式視圖,優先使用 @ViewBuilderGroup
  • 在傳遞資料進出 actor 時忽略 Sendable 要求

參考資料

請參閱技能:swift-actor-persistence 了解基於 actor 的持久化模式。
請參閱技能:swift-protocol-di-testing 了解基於協定的 DI 與使用 Swift Testing 進行測試。