SwiftUI 数据流与单向架构
发布: (2025年12月13日 GMT+8 07:18)
4 min read
原文: Dev.to
Source: Dev.to
什么是一维数据流
- 用户操作 → 事件 向下传递层级。
- 状态 向上返回层级。
- 没有捷径、没有回路、没有意外的变更。
什么会破坏数据流
- 视图直接修改全局状态
- 服务自行更新 UI 状态
- 多个 ViewModel 编辑同一数据
- 将业务逻辑直接绑定到视图
- 在功能边界之间传递绑定
只要有多个对象可以改变同一状态,错误就不可避免。
四层架构
| 层 | 职责 |
|---|---|
| Views | 显示状态,发送用户意图(不包含业务逻辑) |
| ViewModel | 拥有状态,处理意图,协调异步工作 |
| Services | 执行副作用(网络、持久化、分析) |
| Models | 纯数据,无逻辑,无副作用 |
每一层只有单一职责。
视图:发送意图,不要修改状态
Button("Like") {
viewModel.likeTapped()
}
而不是这样:
viewModel.post.likes += 1 // ❌
基于意图的 API 能让逻辑集中且易于测试。
示例 ViewModel
@Observable
class FeedViewModel {
var posts: [Post] = []
var loading = false
func load() async { … }
func likeTapped(postID: String) { … }
}
状态管理规则
- Views 读取状态。
- ViewModels 变更状态。
- Services 永不触碰 UI 状态。
异步工作应放在 ViewModel 中
@MainActor
func load() async {
loading = true
defer { loading = false }
do {
posts = try await api.fetchFeed()
} catch {
errors.present(map(error))
}
}
关键规则
- 始终在主 actor 上更新状态。
- 绝不让 services 直接修改 ViewModel 属性。
- 异步错误通过 ViewModel 向上返回。
Service 设计
良好的 Service 协议
protocol FeedService {
func fetchFeed() async throws -> [Post]
}
不良的 Service 特征
- 持有 UI 状态
- 修改 ViewModel
- 触发导航
- 显示警告
Service 应该 仅 完成工作。
通过状态驱动导航
@Observable
class AppState {
var route: AppRoute?
}
.onChange(of: appState.route) { route in
navigate(route)
}
这样可以让导航保持可预测且易于测试。
危险模式
ChildView(value: $viewModel.someState) // ⚠️
推荐模式
ChildView(onChange: viewModel.childChanged)
绑定用于 UI 组合,而不是跨越架构边界。
测试变得非常简单
func test_like_updates_state() async {
let vm = FeedViewModel(service: MockService())
await vm.load()
vm.likeTapped(postID: "1")
XCTAssertTrue(vm.posts.first!.liked)
}
不需要 UI。
检查清单
“如果我阅读这个文件,能否判断谁拥有状态?”
如果答案不明确 → 架构有问题。
单向数据流的好处
- 可预测的更新
- 更少的 bug
- 更容易处理异步
- 更简洁的测试
- 可扩展的架构
- 清晰的关注点分离
SwiftUI 天生适合这种模型——一旦接受它,一切都会顺畅。