axiom-swiftui-architecture

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SwiftUI Architecture

SwiftUI 架构

When to Use This Skill

何时使用此技能

Use this skill when:
  • You have logic in your SwiftUI view files and want to extract it
  • Choosing between MVVM, TCA, vanilla SwiftUI patterns, or Coordinator
  • Refactoring views to separate concerns
  • Making SwiftUI code testable
  • Asking "where should this code go?"
  • Deciding which property wrapper to use (@State, @Environment, @Bindable)
  • Organizing a SwiftUI codebase for team development
在以下场景使用此技能:
  • SwiftUI视图文件中包含逻辑,需要将其提取出来
  • 在MVVM、TCA、原生SwiftUI模式或Coordinator模式之间做选择
  • 重构视图以实现关注点分离
  • 让SwiftUI代码具备可测试性
  • 提出“这段代码应该放在哪里?”的疑问
  • 决定使用哪种属性包装器(@State、@Environment、@Bindable)
  • 为团队开发组织SwiftUI代码库

Example Prompts

示例提问

What You Might AskWhy This Skill Helps
"There's quite a bit of code in my model view files about logic things. How do I extract it?"Provides refactoring workflow with decision trees for where logic belongs
"Should I use MVVM, TCA, or Apple's vanilla patterns?"Decision criteria based on app complexity, team size, testability needs
"How do I make my SwiftUI code testable?"Shows separation patterns that enable testing without SwiftUI imports
"Where should formatters and calculations go?"Anti-patterns section prevents logic in view bodies
"Which property wrapper do I use?"Decision tree for @State, @Environment, @Bindable, or plain properties
你可能的提问此技能的帮助
“我的模型视图文件里有很多逻辑相关的代码,该如何提取?”提供带有决策树的重构工作流,明确逻辑归属
“我应该使用MVVM、TCA还是苹果的原生模式?”根据应用复杂度、团队规模、可测试性需求提供决策依据
“如何让我的SwiftUI代码可测试?”展示无需导入SwiftUI即可实现测试的分离模式
“格式化工具和计算逻辑应该放在哪里?”通过反模式章节避免逻辑出现在视图主体中
“我应该使用哪种属性包装器?”针对@State、@Environment、@Bindable或普通属性的决策树

Quick Architecture Decision Tree

快速架构决策树

What's driving your architecture choice?
├─ Starting fresh, small/medium app, want Apple's patterns?
│  └─ Use Apple's Native Patterns (Part 1)
│     - @Observable models for business logic
│     - State-as-Bridge for async boundaries
│     - Property wrapper decision tree
├─ Familiar with MVVM from UIKit?
│  └─ Use MVVM Pattern (Part 2)
│     - ViewModels as presentation adapters
│     - Clear View/ViewModel/Model separation
│     - Works well with @Observable
├─ Complex app, need rigorous testability, team consistency?
│  └─ Consider TCA (Part 3)
│     - State/Action/Reducer/Store architecture
│     - Excellent testing story
│     - Learning curve + boilerplate trade-off
└─ Complex navigation, deep linking, multiple entry points?
   └─ Add Coordinator Pattern (Part 4)
      - Can combine with any of the above
      - Extracts navigation logic from views
      - NavigationPath + Coordinator objects

你的架构选择驱动因素是什么?
├─ 从零开始开发,中小型应用,想使用苹果官方模式?
│  └─ 使用苹果原生模式(第一部分)
│     - 用@Observable模型处理业务逻辑
│     - 用State-as-Bridge处理异步边界
│     - 属性包装器决策树
├─ 从UIKit转来,熟悉MVVM?
│  └─ 使用MVVM模式(第二部分)
│     - ViewModel作为展示适配器
│     - 明确的视图/ViewModel/模型分离
│     - 与@Observable兼容良好
├─ 复杂应用,需要严格的可测试性,团队需要一致性?
│  └─ 考虑TCA(第三部分)
│     - 状态/动作/Reducer/Store架构
│     - 出色的测试能力
│     - 学习曲线与样板代码的权衡
└─ 复杂导航、深度链接、多入口点?
   └─ 添加Coordinator模式(第四部分)
      - 可与上述任意模式结合
      - 从视图中提取导航逻辑
      - NavigationPath + Coordinator对象

Part 1: Apple's Native Patterns (iOS 26+)

第一部分:苹果原生模式(iOS 26+)

Core Principle

核心原则

"A data model provides separation between the data and the views that interact with the data. This separation promotes modularity, improves testability, and helps make it easier to reason about how the app works." — Apple Developer Documentation
Apple's modern SwiftUI patterns (WWDC 2023-2025) center on:
  1. @Observable for data models (replaces ObservableObject)
  2. State-as-Bridge for async boundaries (WWDC 2025)
  3. Three property wrappers: @State, @Environment, @Bindable
  4. Synchronous UI updates for animations
“数据模型实现数据与交互视图的分离,这种分离提升了模块化程度,增强了可测试性,也让应用的工作原理更易于理解。” —— 苹果开发者文档
苹果现代SwiftUI模式(WWDC 2023-2025)核心包括:
  1. @Observable 用于数据模型(替代ObservableObject)
  2. State-as-Bridge 用于异步边界(WWDC 2025)
  3. 三种属性包装器:@State、@Environment、@Bindable
  4. 同步UI更新 用于动画效果

The State-as-Bridge Pattern

State-as-Bridge模式

Problem

问题

Async functions create suspension points that can break animations:
swift
// ❌ Problematic: Animation might miss frame deadline
struct ColorExtractorView: View {
    @State private var isLoading = false

    var body: some View {
        Button("Extract Colors") {
            Task {
                isLoading = true  // Synchronous ✅
                await extractColors()  // ⚠️ Suspension point!
                isLoading = false  // ❌ Might happen too late
            }
        }
        .scaleEffect(isLoading ? 1.5 : 1.0)  // ⚠️ Animation timing uncertain
    }
}
异步函数会创建挂起点,可能破坏动画:
swift
// ❌ 有问题:动画可能错过帧截止时间
struct ColorExtractorView: View {
    @State private var isLoading = false

    var body: some View {
        Button("提取颜色") {
            Task {
                isLoading = true  // 同步 ✅
                await extractColors()  // ⚠️ 挂起点!
                isLoading = false  // ❌ 可能执行过晚
            }
        }
        .scaleEffect(isLoading ? 1.5 : 1.0)  // ⚠️ 动画时机不确定
    }
}

Solution: Use State as a Bridge

解决方案:使用State作为桥梁

"Find the boundaries between UI code that requires time-sensitive changes, and long-running async logic."
swift
// ✅ Correct: State bridges UI and async code
@Observable
class ColorExtractor {
    var isLoading = false
    var colors: [Color] = []

    func extract(from image: UIImage) async {
        // This method is async and can live in the model
        let extracted = await heavyComputation(image)
        // Synchronous mutation for UI update
        self.colors = extracted
    }
}

struct ColorExtractorView: View {
    let extractor: ColorExtractor

    var body: some View {
        Button("Extract Colors") {
            // Synchronous state change for animation
            withAnimation {
                extractor.isLoading = true
            }

            // Launch async work
            Task {
                await extractor.extract(from: currentImage)

                // Synchronous state change for animation
                withAnimation {
                    extractor.isLoading = false
                }
            }
        }
        .scaleEffect(extractor.isLoading ? 1.5 : 1.0)
    }
}
Benefits:
  • UI logic stays synchronous (animations work correctly)
  • Async code lives in the model (testable without SwiftUI)
  • Clear boundary between time-sensitive UI and long-running work
“找到需要时间敏感型变更的UI代码与长时间运行的异步逻辑之间的边界。”
swift
// ✅ 正确实现:State作为UI与异步代码的桥梁
@Observable
class ColorExtractor {
    var isLoading = false
    var colors: [Color] = []

    func extract(from image: UIImage) async {
        // 此方法为异步,可放在模型中
        let extracted = await heavyComputation(image)
        // 同步变更以更新UI
        self.colors = extracted
    }
}

struct ColorExtractorView: View {
    let extractor: ColorExtractor

    var body: some View {
        Button("提取颜色") {
            withAnimation {
                extractor.isLoading = true  // ✅ 同步
            }

            // 启动异步任务
            Task {
                await extractor.extract(from: currentImage)

                withAnimation {
                    extractor.isLoading = false  // ✅ 同步
                }
            }
        }
        .scaleEffect(extractor.isLoading ? 1.5 : 1.0)
    }
}
优势:
  • UI逻辑保持同步(动画正常工作)
  • 异步代码位于模型中(无需SwiftUI即可测试)
  • 时间敏感型UI与长时间运行任务的边界清晰

Property Wrapper Decision Tree

属性包装器决策树

There are only 3 questions to answer:
Which property wrapper should I use?
├─ Does this model need to be STATE OF THE VIEW ITSELF?
│  └─ YES → Use @State
│     Examples: Form inputs, local toggles, sheet presentations
│     Lifetime: Managed by the view's lifetime
├─ Does this model need to be part of the GLOBAL ENVIRONMENT?
│  └─ YES → Use @Environment
│     Examples: User account, app settings, dependency injection
│     Lifetime: Lives at app/scene level
├─ Does this model JUST NEED BINDINGS?
│  └─ YES → Use @Bindable
│     Examples: Editing a model passed from parent
│     Lightweight: Only enables $ syntax for bindings
└─ NONE OF THE ABOVE?
   └─ Use as plain property
      Examples: Immutable data, parent-owned models
      No wrapper needed: @Observable handles observation
只需回答3个问题
我应该使用哪种属性包装器?
├─ 该模型是否属于视图自身的状态?
│  └─ 是 → 使用@State
│     示例:表单输入、本地开关、弹窗展示
│     生命周期:由视图生命周期管理
├─ 该模型是否属于全局环境?
│  └─ 是 → 使用@Environment
│     示例:用户账户、应用设置、依赖注入
│     生命周期:应用/场景级别
├─ 该模型是否仅需要绑定?
│  └─ 是 → 使用@Bindable
│     示例:编辑从父视图传入的模型
│     轻量级:仅启用$语法实现绑定
└─ 都不符合?
   └─ 作为普通属性使用
      示例:不可变数据、父视图拥有的模型
      无需包装器:@Observable处理观察

Examples

示例

swift
// ✅ @State — View owns the model
struct DonutEditor: View {
    @State private var donutToAdd = Donut()  // View's own state

    var body: some View {
        TextField("Name", text: $donutToAdd.name)
    }
}

// ✅ @Environment — App-wide model
struct MenuView: View {
    @Environment(Account.self) private var account  // Global

    var body: some View {
        Text("Welcome, \(account.userName)")
    }
}

// ✅ @Bindable — Need bindings to parent-owned model
struct DonutRow: View {
    @Bindable var donut: Donut  // Parent owns it

    var body: some View {
        TextField("Name", text: $donut.name)  // Need binding
    }
}

// ✅ Plain property — Just reading
struct DonutRow: View {
    let donut: Donut  // Parent owns, no binding needed

    var body: some View {
        Text(donut.name)  // Just reading
    }
}
swift
// ✅ @State — 视图拥有模型
struct DonutEditor: View {
    @State private var donutToAdd = Donut()  // 视图自身的状态

    var body: some View {
        TextField("名称", text: $donutToAdd.name)
    }
}

// ✅ @Environment — 应用级模型
struct MenuView: View {
    @Environment(Account.self) private var account  // 全局

    var body: some View {
        Text("欢迎,\(account.userName)")
    }
}

// ✅ @Bindable — 需要绑定到父视图拥有的模型
struct DonutRow: View {
    @Bindable var donut: Donut  // 父视图拥有

    var body: some View {
        TextField("名称", text: $donut.name)  // 需要绑定
    }
}

// ✅ 普通属性 — 仅读取
struct DonutRow: View {
    let donut: Donut  // 父视图拥有,无需绑定

    var body: some View {
        Text(donut.name)  // 仅读取
    }
}

@Observable Model Pattern

@Observable模型模式

Use
@Observable
for business logic that needs to trigger UI updates:
swift
// ✅ Domain model with business logic
@Observable
class FoodTruckModel {
    var orders: [Order] = []
    var donuts = Donut.all

    var orderCount: Int {
        orders.count  // Computed properties work automatically
    }

    func addDonut() {
        donuts.append(Donut())
    }
}

// ✅ View automatically tracks accessed properties
struct DonutMenu: View {
    let model: FoodTruckModel  // No wrapper needed!

    var body: some View {
        List {
            Section("Donuts") {
                ForEach(model.donuts) { donut in
                    Text(donut.name)  // Tracks model.donuts
                }
                Button("Add") {
                    model.addDonut()
                }
            }
            Section("Orders") {
                Text("Count: \(model.orderCount)")  // Tracks model.orders
            }
        }
    }
}
How it works (WWDC 2023/10149):
  • SwiftUI tracks which properties are accessed during
    body
    execution
  • Only those properties trigger view updates when changed
  • Granular dependency tracking = better performance
@Observable
用于需要触发UI更新的业务逻辑:
swift
// ✅ 领域模型与业务逻辑
@Observable
class FoodTruckModel {
    var orders: [Order] = []
    var donuts = Donut.all

    var orderCount: Int {
        orders.count  // 计算属性自动生效
    }

    func addDonut() {
        donuts.append(Donut())
    }
}

// ✅ 视图自动跟踪访问的属性
struct DonutMenu: View {
    let model: FoodTruckModel  // 无需包装器!

    var body: some View {
        List {
            Section("甜甜圈") {
                ForEach(model.donuts) { donut in
                    Text(donut.name)  // 跟踪model.donuts
                }
                Button("添加") {
                    model.addDonut()
                }
            }
            Section("订单") {
                Text("数量:\(model.orderCount)")  // 跟踪model.orders
            }
        }
    }
}
工作原理(WWDC 2023/10149):
  • SwiftUI跟踪
    body
    执行期间访问的属性
  • 仅当这些属性变化时触发视图更新
  • 细粒度依赖跟踪 = 更好的性能

ViewModel Adapter Pattern

ViewModel适配器模式

Use ViewModels as presentation adapters when you need filtering, sorting, or view-specific logic:
swift
// ✅ ViewModel as presentation adapter
@Observable
class PetStoreViewModel {
    let petStore: PetStore  // Domain model
    var searchText: String = ""

    // View-specific computed property
    var filteredPets: [Pet] {
        guard !searchText.isEmpty else { return petStore.myPets }
        return petStore.myPets.filter { $0.name.contains(searchText) }
    }
}

struct PetListView: View {
    @Bindable var viewModel: PetStoreViewModel

    var body: some View {
        List {
            ForEach(viewModel.filteredPets) { pet in
                PetRowView(pet: pet)
            }
        }
        .searchable(text: $viewModel.searchText)
    }
}
When to use a ViewModel adapter:
  • Filtering, sorting, grouping for display
  • Formatting for presentation (but NOT heavy computation)
  • View-specific state that doesn't belong in domain model
  • Bridging between domain model and SwiftUI conventions
When NOT to use a ViewModel:
  • Simple views that just display model data
  • Logic that belongs in the domain model
  • Over-extraction just for "pattern purity"

当需要过滤、排序或视图特定逻辑时,将ViewModel作为展示适配器使用:
swift
// ✅ ViewModel作为展示适配器
@Observable
class PetStoreViewModel {
    let petStore: PetStore  // 领域模型
    var searchText: String = ""

    // 视图特定的计算属性
    var filteredPets: [Pet] {
        guard !searchText.isEmpty else { return petStore.myPets }
        return petStore.myPets.filter { $0.name.contains(searchText) }
    }
}

struct PetListView: View {
    @Bindable var viewModel: PetStoreViewModel

    var body: some View {
        List {
            ForEach(viewModel.filteredPets) { pet in
                PetRowView(pet: pet)
            }
        }
        .searchable(text: $viewModel.searchText)
    }
}
何时使用ViewModel适配器:
  • 用于展示的过滤、排序、分组
  • 展示格式处理(但不包括繁重计算)
  • 不属于领域模型的视图特定状态
  • 领域模型与SwiftUI约定之间的桥接
何时不使用ViewModel:
  • 仅展示模型数据的简单视图
  • 属于领域模型的逻辑
  • 仅为“模式纯度”而过度提取

Part 2: MVVM Pattern

第二部分:MVVM模式

When to Use MVVM

何时使用MVVM

MVVM (Model-View-ViewModel) is appropriate when:
You're familiar with it from UIKit — Easier onboarding for team ✅ You want explicit View/ViewModel separation — Clear contracts ✅ You have complex presentation logic — Multiple filtering/sorting operations ✅ You're migrating from UIKit — Familiar mental model
Avoid MVVM when:
  • Views are simple (just displaying data)
  • You're starting fresh with SwiftUI (Apple's patterns are simpler)
  • You're creating unnecessary abstraction layers
MVVM(模型-视图-ViewModel)适用于以下场景:
你从UIKit过渡而来,熟悉MVVM —— 团队上手更容易 ✅ 你需要明确的视图/ViewModel分离 —— 清晰的契约 ✅ 你有复杂的展示逻辑 —— 多轮过滤/排序操作 ✅ 你正在从UIKit迁移 —— 熟悉的思维模型
避免使用MVVM的场景:
  • 视图简单(仅展示数据)
  • 从零开始开发SwiftUI应用(苹果原生模式更简单)
  • 你在创建不必要的抽象层

MVVM Structure for SwiftUI

SwiftUI中的MVVM结构

swift
// Model — Domain data and business logic
struct Pet: Identifiable {
    let id: UUID
    var name: String
    var kind: Kind
    var trick: String
    var hasAward: Bool = false

    mutating func giveAward() {
        hasAward = true
    }
}

// ViewModel — Presentation logic
@Observable
class PetListViewModel {
    private let petStore: PetStore

    var pets: [Pet] { petStore.myPets }
    var searchText: String = ""
    var selectedSort: SortOption = .name

    var filteredSortedPets: [Pet] {
        let filtered = pets.filter { pet in
            searchText.isEmpty || pet.name.contains(searchText)
        }
        return filtered.sorted { lhs, rhs in
            switch selectedSort {
            case .name: lhs.name < rhs.name
            case .kind: lhs.kind.rawValue < rhs.kind.rawValue
            }
        }
    }

    init(petStore: PetStore) {
        self.petStore = petStore
    }

    func awardPet(_ pet: Pet) {
        petStore.awardPet(pet.id)
    }
}

// View — UI only
struct PetListView: View {
    @Bindable var viewModel: PetListViewModel

    var body: some View {
        List {
            ForEach(viewModel.filteredSortedPets) { pet in
                PetRow(pet: pet) {
                    viewModel.awardPet(pet)
                }
            }
        }
        .searchable(text: $viewModel.searchText)
    }
}
swift
// Model — 领域数据与业务逻辑
struct Pet: Identifiable {
    let id: UUID
    var name: String
    var kind: Kind
    var trick: String
    var hasAward: Bool = false

    mutating func giveAward() {
        hasAward = true
    }
}

// ViewModel — 展示逻辑
@Observable
class PetListViewModel {
    private let petStore: PetStore

    var pets: [Pet] { petStore.myPets }
    var searchText: String = ""
    var selectedSort: SortOption = .name

    var filteredSortedPets: [Pet] {
        let filtered = pets.filter { pet in
            searchText.isEmpty || pet.name.contains(searchText)
        }
        return filtered.sorted { lhs, rhs in
            switch selectedSort {
            case .name: lhs.name < rhs.name
            case .kind: lhs.kind.rawValue < rhs.kind.rawValue
            }
        }
    }

    init(petStore: PetStore) {
        self.petStore = petStore
    }

    func awardPet(_ pet: Pet) {
        petStore.awardPet(pet.id)
    }
}

// View — 仅UI
struct PetListView: View {
    @Bindable var viewModel: PetListViewModel

    var body: some View {
        List {
            ForEach(viewModel.filteredSortedPets) { pet in
                PetRow(pet: pet) {
                    viewModel.awardPet(pet)
                }
            }
        }
        .searchable(text: $viewModel.searchText)
    }
}

Common MVVM Mistakes in SwiftUI

SwiftUI中MVVM的常见错误

❌ Mistake 1: Duplicating @Observable in View and ViewModel

❌ 错误1:在视图和ViewModel中重复使用@Observable

swift
// ❌ Don't do this
@Observable
class MyViewModel {
    var data: String = ""
}

struct MyView: View {
    @State private var viewModel = MyViewModel()  // ❌ Redundant
    // ...
}
swift
// ✅ Correct: Just use @Observable
@Observable
class MyViewModel {
    var data: String = ""
}

struct MyView: View {
    let viewModel: MyViewModel  // ✅ Or @State if view owns it
    // ...
}
swift
// ❌ 不要这样做
@Observable
class MyViewModel {
    var data: String = ""
}

struct MyView: View {
    @State private var viewModel = MyViewModel()  // ❌ 冗余
    // ...
}
swift
// ✅ 正确用法:仅使用@Observable
@Observable
class MyViewModel {
    var data: String = ""
}

struct MyView: View {
    let viewModel: MyViewModel  // ✅ 若视图拥有则用@State
    // ...
}

❌ Mistake 2: God ViewModel

❌ 错误2:上帝ViewModel

swift
// ❌ Don't do this
@Observable
class AppViewModel {
    // Settings
    var isDarkMode = false
    var notificationsEnabled = true

    // User
    var userName = ""
    var userEmail = ""

    // Content
    var posts: [Post] = []
    var comments: [Comment] = []

    // ... 50 more properties
}
swift
// ✅ Correct: Separate concerns
@Observable
class SettingsViewModel {
    var isDarkMode = false
    var notificationsEnabled = true
}

@Observable
class UserProfileViewModel {
    var user: User
}

@Observable
class FeedViewModel {
    var posts: [Post] = []
}
swift
// ❌ 不要这样做
@Observable
class AppViewModel {
    // 设置
    var isDarkMode = false
    var notificationsEnabled = true

    // 用户
    var userName = ""
    var userEmail = ""

    // 内容
    var posts: [Post] = []
    var comments: [Comment] = []

    // ... 还有50多个属性
}
swift
// ✅ 正确用法:分离关注点
@Observable
class SettingsViewModel {
    var isDarkMode = false
    var notificationsEnabled = true
}

@Observable
class UserProfileViewModel {
    var user: User
}

@Observable
class FeedViewModel {
    var posts: [Post] = []
}

❌ Mistake 3: Business Logic in ViewModel

❌ 错误3:业务逻辑放在ViewModel中

swift
// ❌ Business logic shouldn't be in ViewModel
@Observable
class OrderViewModel {
    func calculateDiscount(for order: Order) -> Double {
        // Complex business rules...
        return discount
    }
}
swift
// ✅ Business logic in Model
struct Order {
    func calculateDiscount() -> Double {
        // Complex business rules...
        return discount
    }
}

@Observable
class OrderViewModel {
    let order: Order

    var displayDiscount: String {
        "$\(order.calculateDiscount(), specifier: "%.2f")"  // Just formatting
    }
}

swift
// ❌ 业务逻辑不应放在ViewModel
@Observable
class OrderViewModel {
    func calculateDiscount(for order: Order) -> Double {
        // 复杂业务规则...
        return discount
    }
}
swift
// ✅ 业务逻辑放在Model中
struct Order {
    func calculateDiscount() -> Double {
        // 复杂业务规则...
        return discount
    }
}

@Observable
class OrderViewModel {
    let order: Order

    var displayDiscount: String {
        "$\(order.calculateDiscount(), specifier: "%.2f")"  // 仅格式处理
    }
}

Part 3: TCA (Composable Architecture)

第三部分:TCA(可组合架构)

When to Consider TCA

何时考虑使用TCA

TCA is a third-party architecture from Point-Free. Consider it when:
Rigorous testability is critical — TestStore makes testing deterministic ✅ Large team needs consistency — Strict patterns reduce variation ✅ Complex state management — Side effects, dependencies, composition ✅ You value Redux-like patterns — Unidirectional data flow
Avoid TCA when:
  • Small app or prototype (too much overhead)
  • Team unfamiliar with functional programming
  • You need rapid iteration (boilerplate slows development)
  • You want minimal dependencies
TCA是Point-Free推出的第三方架构,在以下场景考虑使用:
严格的可测试性至关重要 —— TestStore实现确定性测试 ✅ 大型团队需要一致性 —— 严格的模式减少差异 ✅ 复杂状态管理 —— 副作用、依赖、组合 ✅ 你偏好类Redux模式 —— 单向数据流
避免使用TCA的场景:
  • 小型应用或原型(开销过大)
  • 团队不熟悉函数式编程
  • 你需要快速迭代(样板代码拖慢开发)
  • 你希望依赖最少

TCA Core Concepts

TCA核心概念

State

状态(State)

Data your feature needs to perform logic and render UI:
swift
@ObservableState
struct CounterFeature {
    var count = 0
    var fact: String?
    var isLoading = false
}
功能所需的、用于执行逻辑和渲染UI的数据:
swift
@ObservableState
struct CounterFeature {
    var count = 0
    var fact: String?
    var isLoading = false
}

Action

动作(Action)

All possible events in your feature:
swift
enum Action {
    case incrementButtonTapped
    case decrementButtonTapped
    case factButtonTapped
    case factResponse(String)
}
功能中所有可能的事件:
swift
enum Action {
    case incrementButtonTapped
    case decrementButtonTapped
    case factButtonTapped
    case factResponse(String)
}

Reducer

归约器(Reducer)

Describes how state evolves in response to actions:
swift
struct CounterFeature: Reducer {
    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .incrementButtonTapped:
                state.count += 1
                return .none

            case .decrementButtonTapped:
                state.count -= 1
                return .none

            case .factButtonTapped:
                state.isLoading = true
                return .run { [count = state.count] send in
                    let fact = try await numberFact(count)
                    await send(.factResponse(fact))
                }

            case let .factResponse(fact):
                state.isLoading = false
                state.fact = fact
                return .none
            }
        }
    }
}
描述状态如何响应动作而演变:
swift
struct CounterFeature: Reducer {
    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .incrementButtonTapped:
                state.count += 1
                return .none

            case .decrementButtonTapped:
                state.count -= 1
                return .none

            case .factButtonTapped:
                state.isLoading = true
                return .run { [count = state.count] send in
                    let fact = try await numberFact(count)
                    await send(.factResponse(fact))
                }

            case let .factResponse(fact):
                state.isLoading = false
                state.fact = fact
                return .none
            }
        }
    }
}

Store

存储(Store)

Runtime engine that receives actions, executes reducer, handles effects:
swift
struct CounterView: View {
    let store: StoreOf<CounterFeature>

    var body: some View {
        VStack {
            Text("\(store.count)")
            Button("Increment") {
                store.send(.incrementButtonTapped)
            }
        }
    }
}
运行时引擎,接收动作、执行归约器、处理副作用:
swift
struct CounterView: View {
    let store: StoreOf<CounterFeature>

    var body: some View {
        VStack {
            Text("\(store.count)")
            Button("增加") {
                store.send(.incrementButtonTapped)
            }
        }
    }
}

TCA Trade-offs

TCA的权衡

✅ Benefits

✅ 优势

BenefitDescription
TestabilityTestStore makes testing deterministic and exhaustive
ConsistencyOne pattern for all features reduces cognitive load
CompositionSmall reducers combine into larger features
Side effectsStructured effect management (networking, timers, etc.)
优势描述
可测试性TestStore实现确定性和全面测试
一致性所有功能使用同一模式,降低认知负荷
组合性小型归约器组合成大型功能
副作用管理结构化的副作用管理(网络、定时器等)

❌ Costs

❌ 成本

CostDescription
BoilerplateState/Action/Reducer for every feature
Learning curveConcepts from functional programming (effects, dependencies)
DependencyThird-party library, not Apple-supported
Iteration speedMore code to write for simple features
成本描述
样板代码每个功能都需要State/Action/Reducer
学习曲线函数式编程概念(副作用、依赖)
依赖第三方第三方库,非苹果官方支持
迭代速度简单功能需要编写更多代码

When to Choose TCA Over Apple Patterns

何时选择TCA而非苹果原生模式

ScenarioRecommendation
Small app (< 10 screens)Apple patterns (simpler)
Medium app, experienced teamTCA if testability is priority
Large app, multiple teamsTCA for consistency
Rapid prototypingApple patterns (faster)
Mission-critical (banking, health)TCA for rigorous testing

场景推荐方案
小型应用(<10个页面)苹果原生模式(更简单)
中型应用,团队经验丰富若可测试性为优先则选TCA
大型应用,多团队协作TCA保障一致性
快速原型开发苹果原生模式(更快)
关键业务应用(银行、健康)TCA实现严格测试

Part 4: Coordinator Pattern

第四部分:Coordinator模式

When to Use Coordinators

何时使用Coordinator

Coordinators extract navigation logic from views. Use when:
Complex navigation — Multiple paths, conditional flows ✅ Deep linking — URL-driven navigation to any screen ✅ Multiple entry points — Same screen from different contexts ✅ Testable navigation — Isolate navigation from UI
Coordinator从视图中提取导航逻辑,适用于以下场景:
复杂导航 —— 多路径、条件流程 ✅ 深度链接 —— URL驱动导航至任意页面 ✅ 多入口点 —— 同一页面可从不同上下文进入 ✅ 可测试的导航 —— 导航与UI隔离

SwiftUI Coordinator Implementation

SwiftUI中Coordinator的实现

swift
// Navigation destinations
enum Route: Hashable {
    case detail(Pet)
    case settings
    case profile(User)
}

// Coordinator manages navigation state
@Observable
class AppCoordinator {
    var path: [Route] = []

    func showDetail(for pet: Pet) {
        path.append(.detail(pet))
    }

    func showSettings() {
        path.append(.settings)
    }

    func popToRoot() {
        path.removeAll()
    }

    func handleDeepLink(_ url: URL) {
        // Parse URL and build path
        if url.path == "/pets/123" {
            let pet = loadPet(id: "123")
            path = [.detail(pet)]
        }
    }
}

// Root view with NavigationStack
struct AppView: View {
    @State private var coordinator = AppCoordinator()

    var body: some View {
        NavigationStack(path: $coordinator.path) {
            PetListView(coordinator: coordinator)
                .navigationDestination(for: Route.self) { route in
                    switch route {
                    case .detail(let pet):
                        PetDetailView(pet: pet, coordinator: coordinator)
                    case .settings:
                        SettingsView(coordinator: coordinator)
                    case .profile(let user):
                        ProfileView(user: user, coordinator: coordinator)
                    }
                }
        }
        .onOpenURL { url in
            coordinator.handleDeepLink(url)
        }
    }
}

// Views use coordinator instead of NavigationLink
struct PetListView: View {
    let coordinator: AppCoordinator
    let pets: [Pet]

    var body: some View {
        List(pets) { pet in
            Button(pet.name) {
                coordinator.showDetail(for: pet)
            }
        }
    }
}
swift
// 导航目标
enum Route: Hashable {
    case detail(Pet)
    case settings
    case profile(User)
}

// Coordinator管理导航状态
@Observable
class AppCoordinator {
    var path: [Route] = []

    func showDetail(for pet: Pet) {
        path.append(.detail(pet))
    }

    func showSettings() {
        path.append(.settings)
    }

    func popToRoot() {
        path.removeAll()
    }

    func handleDeepLink(_ url: URL) {
        // 解析URL并构建路径
        if url.path == "/pets/123" {
            let pet = loadPet(id: "123")
            path = [.detail(pet)]
        }
    }
}

// 带NavigationStack的根视图
struct AppView: View {
    @State private var coordinator = AppCoordinator()

    var body: some View {
        NavigationStack(path: $coordinator.path) {
            PetListView(coordinator: coordinator)
                .navigationDestination(for: Route.self) { route in
                    switch route {
                    case .detail(let pet):
                        PetDetailView(pet: pet, coordinator: coordinator)
                    case .settings:
                        SettingsView(coordinator: coordinator)
                    case .profile(let user):
                        ProfileView(user: user, coordinator: coordinator)
                    }
                }
        }
        .onOpenURL { url in
            coordinator.handleDeepLink(url)
        }
    }
}

// 视图使用coordinator而非NavigationLink
struct PetListView: View {
    let coordinator: AppCoordinator
    let pets: [Pet]

    var body: some View {
        List(pets) { pet in
            Button(pet.name) {
                coordinator.showDetail(for: pet)
            }
        }
    }
}

Coordinator + Architecture Combinations

Coordinator + 架构组合

You can combine Coordinators with any architecture:
PatternCoordinator Role
Apple NativeCoordinator manages path, @Observable models for data
MVVMCoordinator manages path, ViewModels for presentation
TCACoordinator manages path, Reducers for features

你可以将Coordinator与任何架构组合使用:
模式Coordinator角色
苹果原生Coordinator管理路径,@Observable模型处理数据
MVVMCoordinator管理路径,ViewModel处理展示
TCACoordinator管理路径,Reducer处理功能

Part 5: Refactoring Workflow

第五部分:重构工作流

Step 1: Identify Logic in Views

步骤1:识别视图中的逻辑

Run this checklist on your views:
View body contains:
  • DateFormatter, NumberFormatter creation
  • Calculations or data transformations
  • API calls or async operations
  • Business rules (discounts, validation, etc.)
  • Data filtering or sorting
  • Heavy string manipulation
  • Task { } with complex logic inside
If ANY of these are present, that logic should likely move out.
对你的视图运行以下检查清单:
视图主体包含:
  • DateFormatter、NumberFormatter的创建
  • 计算或数据转换
  • API调用或异步操作
  • 业务规则(折扣、验证等)
  • 数据过滤或排序
  • 繁重的字符串操作
  • 包含复杂逻辑的Task { }
如果存在以上任意一项,该逻辑应移出视图。

Step 2: Extract to Appropriate Layer

步骤2:提取至合适的层级

Use this decision tree:
Where does this logic belong?
├─ Pure domain logic (discounts, validation, business rules)?
│  └─ Extract to Model
│     Example: Order.calculateDiscount()
├─ Presentation logic (filtering, sorting, formatting)?
│  └─ Extract to ViewModel or computed property
│     Example: filteredItems, displayPrice
├─ External side effects (API, database, file system)?
│  └─ Extract to Service
│     Example: APIClient, DatabaseManager
└─ Just expensive computation?
   └─ Cache with @State or create once
      Example: let formatter = DateFormatter()
使用以下决策树:
该逻辑属于哪里?
├─ 纯领域逻辑(折扣、验证、业务规则)?
│  └─ 提取至Model
│     示例:Order.calculateDiscount()
├─ 展示逻辑(过滤、排序、格式化)?
│  └─ 提取至ViewModel或计算属性
│     示例:filteredItems、displayPrice
├─ 外部副作用(API、数据库、文件系统)?
│  └─ 提取至Service
│     示例:APIClient、DatabaseManager
└─ 仅为繁重计算?
   └─ 用@State缓存或创建一次
      示例:let formatter = DateFormatter()

Example: Refactoring Logic from View

示例:从视图中提取逻辑

swift
// ❌ Before: Logic in view body
struct OrderListView: View {
    let orders: [Order]

    var body: some View {
        let formatter = NumberFormatter()  // ❌ Created every render
        formatter.numberStyle = .currency

        let discounted = orders.filter { order in  // ❌ Computed every render
            let discount = order.total * 0.1  // ❌ Business logic in view
            return discount > 10.0
        }

        return List(discounted) { order in
            Text(formatter.string(from: order.total)!)  // ❌ Force unwrap
        }
    }
}
swift
// ✅ After: Logic extracted

// Model — Business logic
struct Order {
    let id: UUID
    let total: Decimal

    var discount: Decimal {
        total * 0.1
    }

    var qualifiesForDiscount: Bool {
        discount > 10.0
    }
}

// ViewModel — Presentation logic
@Observable
class OrderListViewModel {
    let orders: [Order]
    private let formatter: NumberFormatter  // ✅ Created once

    var discountedOrders: [Order] {  // ✅ Computed property
        orders.filter { $0.qualifiesForDiscount }
    }

    init(orders: [Order]) {
        self.orders = orders
        self.formatter = NumberFormatter()
        formatter.numberStyle = .currency
    }

    func formattedTotal(_ order: Order) -> String {
        formatter.string(from: order.total as NSNumber) ?? "$0.00"
    }
}

// View — UI only
struct OrderListView: View {
    let viewModel: OrderListViewModel

    var body: some View {
        List(viewModel.discountedOrders) { order in
            Text(viewModel.formattedTotal(order))
        }
    }
}
swift
// ❌ 重构前:逻辑在视图主体中
struct OrderListView: View {
    let orders: [Order]

    var body: some View {
        let formatter = NumberFormatter()  // ❌ 每次渲染都创建
        formatter.numberStyle = .currency

        let discounted = orders.filter { order in  // ❌ 每次渲染都计算
            let discount = order.total * 0.1  // ❌ 业务逻辑在视图中
            return discount > 10.0
        }

        return List(discounted) { order in
            Text(formatter.string(from: order.total)!)  // ❌ 强制解包
        }
    }
}
swift
// ✅ 重构后:逻辑已提取

// Model — 业务逻辑
struct Order {
    let id: UUID
    let total: Decimal

    var discount: Decimal {
        total * 0.1
    }

    var qualifiesForDiscount: Bool {
        discount > 10.0
    }
}

// ViewModel — 展示逻辑
@Observable
class OrderListViewModel {
    let orders: [Order]
    private let formatter: NumberFormatter  // ✅ 仅创建一次

    var discountedOrders: [Order] {  // ✅ 计算属性
        orders.filter { $0.qualifiesForDiscount }
    }

    init(orders: [Order]) {
        self.orders = orders
        self.formatter = NumberFormatter()
        formatter.numberStyle = .currency
    }

    func formattedTotal(_ order: Order) -> String {
        formatter.string(from: order.total as NSNumber) ?? "$0.00"
    }
}

// View — 仅UI
struct OrderListView: View {
    let viewModel: OrderListViewModel

    var body: some View {
        List(viewModel.discountedOrders) { order in
            Text(viewModel.formattedTotal(order))
        }
    }
}

Step 3: Verify Testability

步骤3:验证可测试性

Your refactoring succeeded if:
swift
// ✅ Can test without importing SwiftUI
import XCTest

final class OrderTests: XCTestCase {
    func testDiscountCalculation() {
        let order = Order(id: UUID(), total: 100)
        XCTAssertEqual(order.discount, 10)
    }

    func testQualifiesForDiscount() {
        let order = Order(id: UUID(), total: 100)
        XCTAssertTrue(order.qualifiesForDiscount)
    }
}

final class OrderViewModelTests: XCTestCase {
    func testFilteredOrders() {
        let orders = [
            Order(id: UUID(), total: 50),   // Discount: 5 ❌
            Order(id: UUID(), total: 200),  // Discount: 20 ✅
        ]
        let viewModel = OrderListViewModel(orders: orders)

        XCTAssertEqual(viewModel.discountedOrders.count, 1)
    }
}
如果满足以下条件,说明重构成功:
swift
// ✅ 无需导入SwiftUI即可测试
import XCTest

final class OrderTests: XCTestCase {
    func testDiscountCalculation() {
        let order = Order(id: UUID(), total: 100)
        XCTAssertEqual(order.discount, 10)
    }

    func testQualifiesForDiscount() {
        let order = Order(id: UUID(), total: 100)
        XCTAssertTrue(order.qualifiesForDiscount)
    }
}

final class OrderViewModelTests: XCTestCase {
    func testFilteredOrders() {
        let orders = [
            Order(id: UUID(), total: 50),   // 折扣:5 ❌
            Order(id: UUID(), total: 200),  // 折扣:20 ✅
        ]
        let viewModel = OrderListViewModel(orders: orders)

        XCTAssertEqual(viewModel.discountedOrders.count, 1)
    }
}

Step 4: Update View Bindings

步骤4:更新视图绑定

After extraction, update property wrappers:
swift
// Before refactoring
struct OrderListView: View {
    @State private var orders: [Order] = []  // View owned
    // ... logic in body
}

// After refactoring
struct OrderListView: View {
    @State private var viewModel: OrderListViewModel  // View owns ViewModel

    init(orders: [Order]) {
        _viewModel = State(initialValue: OrderListViewModel(orders: orders))
    }
}

// Or if parent owns it
struct OrderListView: View {
    let viewModel: OrderListViewModel  // Parent owns, just reading
}

// Or if need bindings
struct OrderListView: View {
    @Bindable var viewModel: OrderListViewModel  // Parent owns, need $
}

提取后,更新属性包装器:
swift
// 重构前
struct OrderListView: View {
    @State private var orders: [Order] = []  // 视图拥有
    // ... 主体中的逻辑
}

// 重构后
struct OrderListView: View {
    @State private var viewModel: OrderListViewModel  // 视图拥有ViewModel

    init(orders: [Order]) {
        _viewModel = State(initialValue: OrderListViewModel(orders: orders))
    }
}

// 若父视图拥有ViewModel
struct OrderListView: View {
    let viewModel: OrderListViewModel  // 父视图拥有,仅读取
}

// 若需要绑定
struct OrderListView: View {
    @Bindable var viewModel: OrderListViewModel  // 父视图拥有,需要$
}

Anti-Patterns (DO NOT DO THIS)

反模式(切勿这样做)

❌ Anti-Pattern 1: Logic in View Body

❌ 反模式1:视图主体中包含逻辑

swift
// ❌ Don't do this
struct ProductListView: View {
    let products: [Product]

    var body: some View {
        let formatter = NumberFormatter()  // ❌ Created every render!
        formatter.numberStyle = .currency

        let sorted = products.sorted { $0.price > $1.price }  // ❌ Sorted every render!

        return List(sorted) { product in
            Text("\(product.name): \(formatter.string(from: product.price)!)")
        }
    }
}
Why it's wrong:
  • formatter
    created on every render (performance)
  • sorted
    computed on every render (performance)
  • Business logic (
    sorted
    ) lives in view (not testable)
  • Force unwrap ('!') can crash
swift
// ✅ Correct
@Observable
class ProductListViewModel {
    let products: [Product]
    private let formatter = NumberFormatter()

    var sortedProducts: [Product] {
        products.sorted { $0.price > $1.price }
    }

    init(products: [Product]) {
        self.products = products
        formatter.numberStyle = .currency
    }

    func formattedPrice(_ product: Product) -> String {
        formatter.string(from: product.price as NSNumber) ?? "$0.00"
    }
}

struct ProductListView: View {
    let viewModel: ProductListViewModel

    var body: some View {
        List(viewModel.sortedProducts) { product in
            Text("\(product.name): \(viewModel.formattedPrice(product))")
        }
    }
}
swift
// ❌ 不要这样做
struct ProductListView: View {
    let products: [Product]

    var body: some View {
        let formatter = NumberFormatter()  // ❌ 每次渲染都创建!
        formatter.numberStyle = .currency

        let sorted = products.sorted { $0.price > $1.price }  // ❌ 每次渲染都排序!

        return List(sorted) { product in
            Text("\(product.name): \(formatter.string(from: product.price)!)")
        }
    }
}
错误原因:
  • formatter
    每次渲染都创建(性能问题)
  • sorted
    每次渲染都计算(性能问题)
  • 业务逻辑不可测试
  • 强制解包('!')可能导致崩溃
swift
// ✅ 正确实现
@Observable
class ProductListViewModel {
    let products: [Product]
    private let formatter = NumberFormatter()

    var sortedProducts: [Product] {
        products.sorted { $0.price > $1.price }
    }

    init(products: [Product]) {
        self.products = products
        formatter.numberStyle = .currency
    }

    func formattedPrice(_ product: Product) -> String {
        formatter.string(from: product.price as NSNumber) ?? "$0.00"
    }
}

struct ProductListView: View {
    let viewModel: ProductListViewModel

    var body: some View {
        List(viewModel.sortedProducts) { product in
            Text("\(product.name): \(viewModel.formattedPrice(product))")
        }
    }
}

❌ Anti-Pattern 2: Async Code Without Boundaries

❌ 反模式2:无边界的异步代码

"Synchronous updates are important for a good user experience."
swift
// ❌ Don't do this
struct ColorExtractorView: View {
    @State private var colors: [Color] = []
    @State private var isLoading = false

    var body: some View {
        Button("Extract") {
            Task {
                isLoading = true
                await heavyExtraction()  // ⚠️ Suspension point
                isLoading = false  // ❌ Animation might break
            }
        }
        .scaleEffect(isLoading ? 1.5 : 1.0)  // ⚠️ Timing issues
    }
}
Why it's wrong:
  • await
    creates suspension point
  • isLoading = false
    might happen after frame deadline
  • Animation timing is unpredictable
swift
// ✅ Correct: State-as-Bridge pattern
@Observable
class ColorExtractor {
    var isLoading = false
    var colors: [Color] = []

    func extract(from image: UIImage) async {
        let extracted = await heavyComputation(image)
        self.colors = extracted  // Synchronous mutation
    }
}

struct ColorExtractorView: View {
    let extractor: ColorExtractor

    var body: some View {
        Button("Extract") {
            withAnimation {
                extractor.isLoading = true  // ✅ Synchronous
            }

            Task {
                await extractor.extract(from: currentImage)

                withAnimation {
                    extractor.isLoading = false  // ✅ Synchronous
                }
            }
        }
        .scaleEffect(extractor.isLoading ? 1.5 : 1.0)
    }
}
“同步更新对良好的用户体验至关重要。”
swift
// ❌ 不要这样做
struct ColorExtractorView: View {
    @State private var colors: [Color] = []
    @State private var isLoading = false

    var body: some View {
        Button("提取") {
            Task {
                isLoading = true
                await heavyExtraction()  // ⚠️ 挂起点
                isLoading = false  // ❌ 动画可能失效
            }
        }
        .scaleEffect(isLoading ? 1.5 : 1.0)  // ⚠️ 时机问题
    }
}
错误原因:
  • await
    创建挂起点
  • isLoading = false
    可能在帧截止时间后执行
  • 动画时机不可预测
swift
// ✅ 正确实现:State-as-Bridge模式
@Observable
class ColorExtractor {
    var isLoading = false
    var colors: [Color] = []

    func extract(from image: UIImage) async {
        let extracted = await heavyComputation(image)
        self.colors = extracted  // 同步变更
    }
}

struct ColorExtractorView: View {
    let extractor: ColorExtractor

    var body: some View {
        Button("提取") {
            withAnimation {
                extractor.isLoading = true  // ✅ 同步
            }

            Task {
                await extractor.extract(from: currentImage)

                withAnimation {
                    extractor.isLoading = false  // ✅ 同步
                }
            }
        }
        .scaleEffect(extractor.isLoading ? 1.5 : 1.0)
    }
}

❌ Anti-Pattern 3: Wrong Property Wrapper

❌ 反模式3:错误的属性包装器

swift
// ❌ Don't use @State for passed-in models
struct DetailView: View {
    @State var item: Item  // ❌ Creates a copy, loses parent changes
}

// ✅ Correct: No wrapper for passed-in models
struct DetailView: View {
    let item: Item  // ✅ Or @Bindable if you need $item
}
swift
// ❌ Don't use @Environment for view-local state
struct FormView: View {
    @Environment(FormData.self) var formData  // ❌ Overkill for local form
}

// ✅ Correct: @State for view-local
struct FormView: View {
    @State private var formData = FormData()  // ✅ View owns it
}
swift
// ❌ 不要对传入的模型使用@State
struct DetailView: View {
    @State var item: Item  // ❌ 创建副本,丢失父视图的变更
}

// ✅ 正确实现:传入的模型无需包装器
struct DetailView: View {
    let item: Item  // ✅ 若需要$item则用@Bindable
}
swift
// ❌ 不要对视图本地状态使用@Environment
struct FormView: View {
    @Environment(FormData.self) var formData  // ❌ 本地表单无需如此复杂
}

// ✅ 正确实现:@State用于视图本地状态
struct FormView: View {
    @State private var formData = FormData()  // ✅ 视图拥有
}

❌ Anti-Pattern 4: God ViewModel

❌ 反模式4:上帝ViewModel

swift
// ❌ Don't create massive ViewModels
@Observable
class AppViewModel {
    // User stuff
    var userName: String
    var userEmail: String

    // Settings stuff
    var isDarkMode: Bool
    var notificationsEnabled: Bool

    // Content stuff
    var posts: [Post]
    var comments: [Comment]

    // ... 50 more properties
}
Why it's wrong:
  • Violates Single Responsibility Principle
  • Hard to test
  • Poor performance (changes anywhere update all views)
  • Difficult to reason about
swift
// ✅ Correct: Separate ViewModels by concern
@Observable class UserViewModel { }
@Observable class SettingsViewModel { }
@Observable class FeedViewModel { }

swift
// ❌ 不要创建庞大的ViewModel
@Observable
class AppViewModel {
    // 用户相关
    var userName: String
    var userEmail: String

    // 设置相关
    var isDarkMode: Bool
    var notificationsEnabled: Bool

    // 内容相关
    var posts: [Post]
    var comments: [Comment]

    // ... 还有50多个属性
}
错误原因:
  • 违反单一职责原则
  • 难以测试
  • 性能差(任意变更都会更新所有视图)
  • 难以理解
swift
// ✅ 正确实现:按关注点分离ViewModel
@Observable class UserViewModel { }
@Observable class SettingsViewModel { }
@Observable class FeedViewModel { }

Code Review Checklist

代码审查清单

Before merging SwiftUI code, verify:
合并SwiftUI代码前,验证以下内容:

Views

视图

  • View bodies contain ONLY UI code (Text, Button, List, etc.)
  • No formatters created in view body
  • No calculations or transformations in view body
  • No API calls or database queries in view body
  • No business rules in view body
  • 视图主体仅包含UI代码(Text、Button、List等)
  • 视图主体中未创建格式化工具
  • 视图主体中无计算或转换逻辑
  • 视图主体中无API调用或数据库查询
  • 视图主体中无业务规则

Logic Separation

逻辑分离

  • Business logic is in models (testable without SwiftUI)
  • Presentation logic is in ViewModels or computed properties
  • Side effects are in services or model methods
  • Heavy computations are cached or computed once
  • 业务逻辑位于模型中(无需SwiftUI即可测试)
  • 展示逻辑位于ViewModel或计算属性中
  • 副作用位于服务或模型方法中
  • 繁重计算已缓存或仅计算一次

Property Wrappers

属性包装器

  • @State for view-owned models
  • @Environment for app-wide models
  • @Bindable when bindings are needed
  • No wrapper when just reading
  • @State用于视图拥有的模型
  • @Environment用于应用级模型
  • 需要绑定时使用@Bindable
  • 仅读取时无需包装器

Animations & Async

动画与异步

  • State changes for animations are synchronous
  • Async boundaries use State-as-Bridge pattern
  • No
    await
    between
    withAnimation { }
    blocks
  • 动画的状态变更为同步
  • 异步边界使用State-as-Bridge模式
  • withAnimation { }
    块之间无
    await

Testability

可测试性

  • Can test business logic without importing SwiftUI
  • Can test ViewModels without rendering views
  • Navigation logic is isolated (if using Coordinators)

  • 无需导入SwiftUI即可测试业务逻辑
  • 无需渲染视图即可测试ViewModel
  • 导航逻辑已隔离(若使用Coordinator)

Pressure Scenarios

压力场景

Scenario 1: "Just put it in the view for now"

场景1:“先把逻辑放在视图里,以后再重构”

The Pressure

压力来源

Manager: "We need this feature by Friday. Just put the logic in the view for now, we'll refactor later."
经理:“我们周五前需要完成这个功能。先把逻辑放在视图里,以后再重构。”

Red Flags

危险信号

If you hear:
  • ❌ "We'll refactor later" (tech debt that never gets paid)
  • ❌ "It's just one view" (views multiply)
  • ❌ "We don't have time for architecture" (costs more later)
如果你听到以下说法:
  • ❌ “以后再重构”(永远不会兑现的技术债务)
  • ❌ “只是一个视图而已”(视图会不断增加)
  • ❌ “我们没时间考虑架构”(后续成本更高)

Time Cost Comparison

时间成本对比

Option A: Put logic in view
  • Write feature in view: 2 hours
  • Realize it's untestable: 1 hour
  • Try to test it anyway: 2 hours
  • Give up, ship with manual testing: 0 hours
  • Total: 5 hours, 0 tests
Option B: Extract logic properly
  • Create model/ViewModel: 30 min
  • Write feature with separation: 2 hours
  • Write tests: 1 hour
  • Total: 3.5 hours, full test coverage
选项A:把逻辑放在视图中
  • 在视图中编写功能:2小时
  • 发现不可测试:1小时
  • 尝试测试:2小时
  • 放弃,手动测试后发布:0小时
  • 总计:5小时,无测试
选项B:正确提取逻辑
  • 创建模型/ViewModel:30分钟
  • 按分离原则编写功能:2小时
  • 编写测试:1小时
  • 总计:3.5小时,完整测试覆盖

How to Push Back Professionally

专业的反驳方式

Step 1: Acknowledge the deadline
"I understand Friday is the deadline. Let me show you why proper separation is actually faster."
Step 2: Show the time comparison
"Putting logic in views takes 5 hours with no tests. Extracting it properly takes 3.5 hours with full tests. We save 1.5 hours AND get tests."
Step 3: Offer the compromise
"If we're truly out of time, I can extract 80% now and mark the remaining 20% as tech debt with a ticket. But let's not skip extraction entirely."
Step 4: Document if pressured to proceed
swift
// TODO: TECH DEBT - Extract business logic to ViewModel
// Ticket: PROJ-123
// Added: 2025-12-14
// Reason: Deadline pressure from manager
// Estimated refactor time: 2 hours
步骤1:认可截止日期
“我理解周五是截止日期。让我说明为什么正确的分离实际上更快。”
步骤2:展示时间对比
“把逻辑放在视图中需要5小时且无测试。正确提取逻辑需要3.5小时且有完整测试。我们节省1.5小时还能获得测试保障。”
步骤3:提出折中方案
“如果时间确实紧张,我可以现在提取80%的逻辑,剩下20%标记为技术债务并创建工单。但不要完全跳过提取步骤。”
步骤4:如果被迫妥协,记录下来
swift
// TODO: 技术债务 - 将业务逻辑提取至ViewModel
// 工单:PROJ-123
// 添加时间:2025-12-14
// 原因:经理要求赶截止日期
// 预计重构时间:2小时

When to Accept

何时接受妥协

Only skip extraction if:
  1. This is a throwaway prototype (deleted next week)
  2. You have explicit time budget for refactoring (scheduled ticket)
  3. The view will never grow beyond 20 lines
仅在以下场景跳过提取:
  1. 这是一次性原型(下周就删除)
  2. 你有明确的重构时间预算(已排期的工单)
  3. 视图永远不会超过20行

Scenario 2: "TCA is overkill, just use vanilla SwiftUI"

场景2:“TCA太冗余了,就用原生SwiftUI”

The Pressure

压力来源

Tech Lead: "TCA is too complex for this project. Just use vanilla SwiftUI with @Observable."
技术负责人:“TCA对这个项目来说太复杂了。就用带@Observable的原生SwiftUI。”

Decision Criteria

决策标准

Ask these questions:
QuestionTCAVanilla
Is testability critical (medical, financial)?
Do you have < 5 screens?
Is team experienced with functional programming?
Do you need rapid prototyping?
Is consistency across large team critical?
Do you have complex side effects (sockets, timers)?~
Recommendation matrix:
  • 4+ checks for TCA → Use TCA
  • 4+ checks for Vanilla → Use Vanilla
  • Tie → Start with Vanilla, migrate to TCA if needed
问自己以下问题:
问题TCA原生SwiftUI
可测试性是否至关重要(医疗、金融)?
应用是否少于5个页面?
团队是否熟悉函数式编程?
你是否需要快速原型开发?
大型团队的一致性是否至关重要?
你是否有复杂的副作用(套接字、定时器)?~
推荐矩阵:
  • 4项及以上符合TCA → 使用TCA
  • 4项及以上符合原生SwiftUI → 使用原生SwiftUI
  • 平局 → 先使用原生SwiftUI,必要时迁移至TCA

How to Push Back

反驳方式

If arguing FOR TCA:
"I understand TCA feels heavy. But we're building a banking app. The TestStore gives us exhaustive testing that catches bugs before production. The 2-week learning curve is worth it for 2 years of maintenance."
If arguing AGAINST TCA:
"I agree TCA is powerful, but we're prototyping features weekly. The boilerplate will slow us down. Let's use @Observable now and migrate to TCA if we prove the features are worth building."
如果你支持使用TCA:
“我理解TCA看起来繁重。但我们正在开发银行应用。TestStore能让我们在生产前发现所有bug。2周的学习曲线换2年的易维护性是值得的。”
如果你反对使用TCA:
“我认同TCA很强大,但我们每周都在原型化新功能。样板代码会拖慢我们。我们先用@Observable,等确认功能值得投入再迁移至TCA。”

Scenario 3: "Refactoring will take too long"

场景3:“重构需要太长时间”

The Pressure

压力来源

PM: "We have 3 features to ship this month. We can't spend 2 weeks refactoring existing views."
产品经理:“我们这个月要发布3个功能。不能花2周时间重构现有视图。”

Incremental Extraction Strategy

增量提取策略

You don't have to refactor everything at once:
Week 1: Extract 1 view
  • Pick the most painful view (lots of logic)
  • Extract to ViewModel
  • Write tests
  • Time: 4 hours
Week 2: Extract 2 views
  • Now you have a pattern to follow
  • Faster than week 1
  • Time: 6 hours
Week 3: New features use proper architecture
  • Don't refactor old code yet
  • All NEW code follows the pattern
  • Time: 0 hours (same as before)
Month 2: Gradually refactor as you touch files
  • Refactor when fixing bugs in old views
  • Refactor when adding features to old views
  • Time: Amortized over feature work
你不需要一次性重构所有内容:
第1周:提取1个视图
  • 选择最棘手的视图(包含大量逻辑)
  • 提取至ViewModel
  • 编写测试
  • 时间:4小时
第2周:提取2个视图
  • 已有可遵循的模式
  • 比第1周更快
  • 时间:6小时
第3周:新功能使用正确架构
  • 暂不重构旧代码
  • 所有新代码遵循模式
  • 时间:0小时(和之前一样)
第2个月:在接触旧文件时逐步重构
  • 修复旧视图的bug时进行重构
  • 为旧视图添加功能时进行重构
  • 时间:分摊到功能开发中

How to Push Back

反驳方式

"I'm not proposing we stop feature work for 2 weeks. I'm proposing:
  1. Week 1: Extract our worst view (the OrdersView with 500 lines)
  2. Week 2: Extract 2 more problematic views
  3. Going forward: All NEW features use proper architecture
  4. We refactor old views when we touch them anyway
This costs 10 hours upfront and saves us 2+ hours per feature going forward."

“我不是提议暂停功能开发2周。我提议:
  1. 第1周:提取最糟糕的视图(有500行代码的OrdersView)
  2. 第2周:提取另外2个有问题的视图
  3. 从现在起:所有新功能使用正确架构
  4. 我们在接触旧文件时顺便重构
前期投入10小时,后续每个功能能节省2小时以上。”

Real-World Impact

实际效果对比

Before: Logic in View

重构前:逻辑在视图中

swift
// 😰 200 lines of pain
struct OrderListView: View {
    @State private var orders: [Order] = []
    @State private var searchText = ""
    @State private var selectedFilter: FilterType = .all

    var body: some View {
        // ❌ Formatters created every render
        let currencyFormatter = NumberFormatter()
        currencyFormatter.numberStyle = .currency

        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .medium

        // ❌ Business logic in view
        let filtered = orders.filter { order in
            if !searchText.isEmpty && !order.customerName.contains(searchText) {
                return false
            }

            switch selectedFilter {
            case .all: return true
            case .pending: return !order.isCompleted
            case .completed: return order.isCompleted
            case .highValue: return order.total > 1000
            }
        }

        // ❌ More business logic
        let sorted = filtered.sorted { lhs, rhs in
            if selectedFilter == .highValue {
                return lhs.total > rhs.total
            } else {
                return lhs.date > rhs.date
            }
        }

        return List(sorted) { order in
            VStack(alignment: .leading) {
                Text(order.customerName)
                Text(currencyFormatter.string(from: order.total as NSNumber)!)
                Text(dateFormatter.string(from: order.date))

                if order.isCompleted {
                    Image(systemName: "checkmark.circle.fill")
                } else {
                    Button("Complete") {
                        // ❌ Async logic in view
                        Task {
                            do {
                                try await completeOrder(order)
                                await loadOrders()
                            } catch {
                                print(error)  // ❌ No error handling
                            }
                        }
                    }
                }
            }
        }
        .searchable(text: $searchText)
        .task {
            await loadOrders()
        }
    }

    func loadOrders() async {
        // ❌ API call in view
        // ... 50 more lines
    }

    func completeOrder(_ order: Order) async throws {
        // ❌ API call in view
        // ... 30 more lines
    }
}
Problems:
  • 200+ lines in one file
  • Formatters created every render (performance)
  • Business logic untestable
  • No error handling
  • Hard to reason about
swift
// 😰 200多行的糟糕实现
struct OrderListView: View {
    @State private var orders: [Order] = []
    @State private var searchText = ""
    @State private var selectedFilter: FilterType = .all

    var body: some View {
        // ❌ 每次渲染都创建格式化工具
        let currencyFormatter = NumberFormatter()
        currencyFormatter.numberStyle = .currency

        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .medium

        // ❌ 业务逻辑在视图中
        let filtered = orders.filter { order in
            if !searchText.isEmpty && !order.customerName.contains(searchText) {
                return false
            }

            switch selectedFilter {
            case .all: return true
            case .pending: return !order.isCompleted
            case .completed: return order.isCompleted
            case .highValue: return order.total > 1000
            }
        }

        // ❌ 更多业务逻辑
        let sorted = filtered.sorted { lhs, rhs in
            if selectedFilter == .highValue {
                return lhs.total > rhs.total
            } else {
                return lhs.date > rhs.date
            }
        }

        return List(sorted) { order in
            VStack(alignment: .leading) {
                Text(order.customerName)
                Text(currencyFormatter.string(from: order.total as NSNumber)!)
                Text(dateFormatter.string(from: order.date))

                if order.isCompleted {
                    Image(systemName: "checkmark.circle.fill")
                } else {
                    Button("完成") {
                        // ❌ 异步逻辑在视图中
                        Task {
                            do {
                                try await completeOrder(order)
                                await loadOrders()
                            } catch {
                                print(error)  // ❌ 无错误处理
                            }
                        }
                    }
                }
            }
        }
        .searchable(text: $searchText)
        .task {
            await loadOrders()
        }
    }

    func loadOrders() async {
        // ❌ API调用在视图中
        // ... 还有50多行
    }

    func completeOrder(_ order: Order) async throws {
        // ❌ API调用在视图中
        // ... 还有30多行
    }
}
问题:
  • 单个文件200+行
  • 每次渲染都创建格式化工具(性能问题)
  • 业务逻辑不可测试
  • 无错误处理
  • 难以理解

After: Proper Architecture

重构后:正确架构

swift
// Model — 30 lines
struct Order {
    let id: UUID
    let customerName: String
    let total: Decimal
    let date: Date
    var isCompleted: Bool

    var isHighValue: Bool {
        total > 1000
    }
}

// ViewModel — 60 lines
@Observable
class OrderListViewModel {
    private let orderService: OrderService
    private let currencyFormatter = NumberFormatter()
    private let dateFormatter = DateFormatter()

    var orders: [Order] = []
    var searchText = ""
    var selectedFilter: FilterType = .all
    var error: Error?

    var filteredOrders: [Order] {
        orders
            .filter(matchesSearch)
            .filter(matchesFilter)
            .sorted(by: sortComparator)
    }

    init(orderService: OrderService) {
        self.orderService = orderService
        currencyFormatter.numberStyle = .currency
        dateFormatter.dateStyle = .medium
    }

    func loadOrders() async {
        do {
            orders = try await orderService.fetchOrders()
        } catch {
            self.error = error
        }
    }

    func completeOrder(_ order: Order) async {
        do {
            try await orderService.complete(order.id)
            await loadOrders()
        } catch {
            self.error = error
        }
    }

    func formattedTotal(_ order: Order) -> String {
        currencyFormatter.string(from: order.total as NSNumber) ?? "$0.00"
    }

    func formattedDate(_ order: Order) -> String {
        dateFormatter.string(from: order.date)
    }

    private func matchesSearch(_ order: Order) -> Bool {
        searchText.isEmpty || order.customerName.contains(searchText)
    }

    private func matchesFilter(_ order: Order) -> Bool {
        switch selectedFilter {
        case .all: true
        case .pending: !order.isCompleted
        case .completed: order.isCompleted
        case .highValue: order.isHighValue
        }
    }

    private func sortComparator(_ lhs: Order, _ rhs: Order) -> Bool {
        selectedFilter == .highValue
            ? lhs.total > rhs.total
            : lhs.date > rhs.date
    }
}

// View — 40 lines
struct OrderListView: View {
    @Bindable var viewModel: OrderListViewModel

    var body: some View {
        List(viewModel.filteredOrders) { order in
            OrderRow(order: order, viewModel: viewModel)
        }
        .searchable(text: $viewModel.searchText)
        .task {
            await viewModel.loadOrders()
        }
        .alert("Error", error: $viewModel.error) { }
    }
}

struct OrderRow: View {
    let order: Order
    let viewModel: OrderListViewModel

    var body: some View {
        VStack(alignment: .leading) {
            Text(order.customerName)
            Text(viewModel.formattedTotal(order))
            Text(viewModel.formattedDate(order))

            if order.isCompleted {
                Image(systemName: "checkmark.circle.fill")
            } else {
                Button("Complete") {
                    Task {
                        await viewModel.completeOrder(order)
                    }
                }
            }
        }
    }
}

// Tests — 100 lines
final class OrderViewModelTests: XCTestCase {
    func testFilterBySearch() async {
        let viewModel = OrderListViewModel(orderService: MockOrderService())
        await viewModel.loadOrders()

        viewModel.searchText = "John"
        XCTAssertEqual(viewModel.filteredOrders.count, 1)
    }

    func testFilterByHighValue() async {
        let viewModel = OrderListViewModel(orderService: MockOrderService())
        await viewModel.loadOrders()

        viewModel.selectedFilter = .highValue
        XCTAssertTrue(viewModel.filteredOrders.allSatisfy { $0.isHighValue })
    }

    // ... 10 more tests
}
Benefits:
  • View: 40 lines (was 200)
  • ViewModel: Fully testable without SwiftUI
  • Model: Pure business logic
  • Formatters: Created once, not every render
  • Error handling: Proper with alerts
  • Tests: 10+ tests covering all logic

swift
// Model — 30行
struct Order {
    let id: UUID
    let customerName: String
    let total: Decimal
    let date: Date
    var isCompleted: Bool

    var isHighValue: Bool {
        total > 1000
    }
}

// ViewModel — 60行
@Observable
class OrderListViewModel {
    private let orderService: OrderService
    private let currencyFormatter = NumberFormatter()
    private let dateFormatter = DateFormatter()

    var orders: [Order] = []
    var searchText = ""
    var selectedFilter: FilterType = .all
    var error: Error?

    var filteredOrders: [Order] {
        orders
            .filter(matchesSearch)
            .filter(matchesFilter)
            .sorted(by: sortComparator)
    }

    init(orderService: OrderService) {
        self.orderService = orderService
        currencyFormatter.numberStyle = .currency
        dateFormatter.dateStyle = .medium
    }

    func loadOrders() async {
        do {
            orders = try await orderService.fetchOrders()
        } catch {
            self.error = error
        }
    }

    func completeOrder(_ order: Order) async {
        do {
            try await orderService.complete(order.id)
            await loadOrders()
        } catch {
            self.error = error
        }
    }

    func formattedTotal(_ order: Order) -> String {
        currencyFormatter.string(from: order.total as NSNumber) ?? "$0.00"
    }

    func formattedDate(_ order: Order) -> String {
        dateFormatter.string(from: order.date)
    }

    private func matchesSearch(_ order: Order) -> Bool {
        searchText.isEmpty || order.customerName.contains(searchText)
    }

    private func matchesFilter(_ order: Order) -> Bool {
        switch selectedFilter {
        case .all: true
        case .pending: !order.isCompleted
        case .completed: order.isCompleted
        case .highValue: order.isHighValue
        }
    }

    private func sortComparator(_ lhs: Order, _ rhs: Order) -> Bool {
        selectedFilter == .highValue
            ? lhs.total > rhs.total
            : lhs.date > rhs.date
    }
}

// View — 40行
struct OrderListView: View {
    @Bindable var viewModel: OrderListViewModel

    var body: some View {
        List(viewModel.filteredOrders) { order in
            OrderRow(order: order, viewModel: viewModel)
        }
        .searchable(text: $viewModel.searchText)
        .task {
            await viewModel.loadOrders()
        }
        .alert("错误", error: $viewModel.error) { }
    }
}

struct OrderRow: View {
    let order: Order
    let viewModel: OrderListViewModel

    var body: some View {
        VStack(alignment: .leading) {
            Text(order.customerName)
            Text(viewModel.formattedTotal(order))
            Text(viewModel.formattedDate(order))

            if order.isCompleted {
                Image(systemName: "checkmark.circle.fill")
            } else {
                Button("完成") {
                    Task {
                        await viewModel.completeOrder(order)
                    }
                }
            }
        }
    }
}

// 测试 — 100行
final class OrderViewModelTests: XCTestCase {
    func testFilterBySearch() async {
        let viewModel = OrderListViewModel(orderService: MockOrderService())
        await viewModel.loadOrders()

        viewModel.searchText = "John"
        XCTAssertEqual(viewModel.filteredOrders.count, 1)
    }

    func testFilterByHighValue() async {
        let viewModel = OrderListViewModel(orderService: MockOrderService())
        await viewModel.loadOrders()

        viewModel.selectedFilter = .highValue
        XCTAssertTrue(viewModel.filteredOrders.allSatisfy { $0.isHighValue })
    }

    // ... 还有10个测试
}
优势:
  • 视图仅40行(原先是200行)
  • ViewModel无需SwiftUI即可完全测试
  • 模型包含纯业务逻辑
  • 格式化工具仅创建一次,而非每次渲染
  • 错误处理完善,带有弹窗
  • 10+个测试覆盖所有逻辑

Resources

资源

WWDC: 2025-266, 2024-10150, 2023-10149, 2023-10160
Docs: /swiftui/managing-model-data-in-your-app
External: github.com/pointfreeco/swift-composable-architecture

Platforms: iOS 26+, iPadOS 26+, macOS Tahoe+, watchOS 26+, axiom-visionOS 26+ Xcode: 26+ Status: Production-ready (v1.0)
WWDC:2025-266, 2024-10150, 2023-10149, 2023-10160
文档:/swiftui/managing-model-data-in-your-app
外部资源:github.com/pointfreeco/swift-composable-architecture

平台:iOS 26+, iPadOS 26+, macOS Tahoe+, watchOS 26+, axiom-visionOS 26+ Xcode:26+ 状态:生产可用(v1.0)