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 天生适合这种模型——一旦接受它,一切都会顺畅。

Back to Blog

相关文章

阅读更多 »

单状态模型架构

问题陈述 现代系统架构常常在追求 scale 和 flexibility 的同时,以牺牲简洁性和一致性为代价。在急于采用 microservices …