axiom-swiftui-architecture
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSwiftUI 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 Ask | Why 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:
- @Observable for data models (replaces ObservableObject)
- State-as-Bridge for async boundaries (WWDC 2025)
- Three property wrappers: @State, @Environment, @Bindable
- Synchronous UI updates for animations
“数据模型实现数据与交互视图的分离,这种分离提升了模块化程度,增强了可测试性,也让应用的工作原理更易于理解。” —— 苹果开发者文档
苹果现代SwiftUI模式(WWDC 2023-2025)核心包括:
- @Observable 用于数据模型(替代ObservableObject)
- State-as-Bridge 用于异步边界(WWDC 2025)
- 三种属性包装器:@State、@Environment、@Bindable
- 同步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 for business logic that needs to trigger UI updates:
@Observableswift
// ✅ 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 execution
body - Only those properties trigger view updates when changed
- Granular dependency tracking = better performance
将用于需要触发UI更新的业务逻辑:
@Observableswift
// ✅ 领域模型与业务逻辑
@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
✅ 优势
| Benefit | Description |
|---|---|
| Testability | TestStore makes testing deterministic and exhaustive |
| Consistency | One pattern for all features reduces cognitive load |
| Composition | Small reducers combine into larger features |
| Side effects | Structured effect management (networking, timers, etc.) |
| 优势 | 描述 |
|---|---|
| 可测试性 | TestStore实现确定性和全面测试 |
| 一致性 | 所有功能使用同一模式,降低认知负荷 |
| 组合性 | 小型归约器组合成大型功能 |
| 副作用管理 | 结构化的副作用管理(网络、定时器等) |
❌ Costs
❌ 成本
| Cost | Description |
|---|---|
| Boilerplate | State/Action/Reducer for every feature |
| Learning curve | Concepts from functional programming (effects, dependencies) |
| Dependency | Third-party library, not Apple-supported |
| Iteration speed | More code to write for simple features |
| 成本 | 描述 |
|---|---|
| 样板代码 | 每个功能都需要State/Action/Reducer |
| 学习曲线 | 函数式编程概念(副作用、依赖) |
| 依赖第三方 | 第三方库,非苹果官方支持 |
| 迭代速度 | 简单功能需要编写更多代码 |
When to Choose TCA Over Apple Patterns
何时选择TCA而非苹果原生模式
| Scenario | Recommendation |
|---|---|
| Small app (< 10 screens) | Apple patterns (simpler) |
| Medium app, experienced team | TCA if testability is priority |
| Large app, multiple teams | TCA for consistency |
| Rapid prototyping | Apple 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:
| Pattern | Coordinator Role |
|---|---|
| Apple Native | Coordinator manages path, @Observable models for data |
| MVVM | Coordinator manages path, ViewModels for presentation |
| TCA | Coordinator manages path, Reducers for features |
你可以将Coordinator与任何架构组合使用:
| 模式 | Coordinator角色 |
|---|---|
| 苹果原生 | Coordinator管理路径,@Observable模型处理数据 |
| MVVM | Coordinator管理路径,ViewModel处理展示 |
| TCA | Coordinator管理路径,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:
- created on every render (performance)
formatter - computed on every render (performance)
sorted - Business logic () lives in view (not testable)
sorted - 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:
- creates suspension point
await - might happen after frame deadline
isLoading = false - 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 between
awaitblockswithAnimation { }
- 动画的状态变更为同步
- 异步边界使用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:
- This is a throwaway prototype (deleted next week)
- You have explicit time budget for refactoring (scheduled ticket)
- The view will never grow beyond 20 lines
仅在以下场景跳过提取:
- 这是一次性原型(下周就删除)
- 你有明确的重构时间预算(已排期的工单)
- 视图永远不会超过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:
| Question | TCA | Vanilla |
|---|---|---|
| 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:
- Week 1: Extract our worst view (the OrdersView with 500 lines)
- Week 2: Extract 2 more problematic views
- Going forward: All NEW features use proper architecture
- 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周:提取最糟糕的视图(有500行代码的OrdersView)
- 第2周:提取另外2个有问题的视图
- 从现在起:所有新功能使用正确架构
- 我们在接触旧文件时顺便重构
前期投入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)