← 返回文章列表

记饺 App 的 SwiftUI 架构演进

2026-06-20 · SwiftUI · iOS · Architecture

记饺是个个人记账 App,最早用 MVVM 快速上线,后来数据复杂度和交互状态增加,改成了 TCA(The Composable Architecture)。这篇记录一下两个阶段的对比和迁移心得。

第一阶段:MVVM

class LedgerViewModel: ObservableObject {
    @Published var records: [Record] = []
    @Published var monthlyTotal: Double = 0

    func addRecord(_ r: Record) {
        records.append(r)
        recalculate()
    }
}

两个 ViewModel 互相依赖时就开始乱:分类页和记账页共用一个 CategoryViewModel,但各自需要的排序逻辑不同。@Published 的变化触发链也变得不可预测。

第二阶段:TCA

struct LedgerReducer: Reducer {
    struct State: Equatable {
        var records: [Record] = []
        var monthlyTotal: Double = 0
        var isLoading = false
    }
    enum Action: Equatable {
        case addRecord(Record)
        case deleteRecord(UUID)
        case loadFromCloud
        case cloudResponse(TaskResult<[Record]>)
    }
    func reduce(into state: inout State, action: Action) -> Effect<Action> {
        switch action {
        case .addRecord(let r):
            state.records.append(r)
            return .none
        case .loadFromCloud:
            state.isLoading = true
            return .run { send in
                await send(.cloudResponse(TaskResult { try await cloud.fetch() }))
            }
        case .cloudResponse(.success(let r)):
            state.records = r
            state.isLoading = false
            return .none
        case .cloudResponse(.failure):
            state.isLoading = false
            return .none
        }
    }
}

对比

维度MVVMTCA
状态管理分散在多个 ViewModel单一 State 树
可测试性需要 mock ViewModel纯函数测试 reducer
副作用散落各处集中到 Effect
调试靠 printReducer 可 replay
学习曲线陡(Pointfree 的库范式)

数据持久化

Core Data + iCloud 同步。大坑是首次同步的合并冲突——两个设备离线各记了几笔,上线后 NSPersistentCloudKitContainer 的默认策略无法合并自定义冲突。最终重写了 resolveConflicts,按时间戳 last-write-wins。

结论:个人项目用 MVVM 够用。一旦涉及复杂状态流和多人协作,TCA 的收益才明显。