MVVM Fundamentals
by Neoren
History及简介
-
2005年由微软提出(SwiftUI 2019年发布),引入SwiftUI后非常受欢迎、被广泛采用
- MVVM强调将应用的视图和逻辑相分离
- MVVM将代码分为模型、视图、视图模型 三部分
- 重要原则:View和Model之间不能直接通信,必须通过ViewModel作为中介
- 间接通信也实现了数据的双向绑定:模型的数据变化会反应到界面上,用户在界面上的操作也会通过ViewModel更新回模型数据
MVVM架构中各层的职责和结构
View层
-
职责:
-
负责 UI 展示 + 交互事件采集
-
只做轻量逻辑:布局、动画、简单的显示分支
-
不直接访问网络/数据库/缓存,不写业务规则
-
-
结构:
- 持有一个ViewModel实例(通过@Observable + @State/@Bindable)
- 用$将输入绑定到vm的状态
- 事件调用:vm.onAppear() (页面出现时加载数据), vm.refresh() (下拉刷新), vm.submit() (表单提交)
- 事件调用里的逻辑应该是业务编排
ViewModel层
-
职责:
-
提供View可直接消费的状态
-
把用户意图转换成动作(对应View中的事件调用,比如:)

-
协调业务流程:(意图转换成动作时)调用 UseCase/Service/Repository (业务编排),进行必要的数据转换、格式化、排序、过滤、权限判断等 (适配/派生)

-
管理异步:Task {}、async/await,以及错误处理、取消(可选)
-
-
结构:
- iOS 17+:@Observable class XxxViewModel { … }
- 将 UI 状态集中成一个类型更清晰:enum ViewState { case idle, loading, loaded([Item]), empty, failed(String) } 或 struct State { var items: [Item]; var isLoading: Bool; … }
- 依赖通过初始化注入(便于测试):init(service: FooServiceProtocol)
- 经验法则:ViewModel 不持有 View(某属性不应指向一个View),也不返回 View(某方法不应返回一个View);它只输出“可渲染的数据 + 可调用的动作”
Model层
-

-
领域模型(Domain Model / Entity)职责:
- 描述业务数据结构与规则(例如 User, Trip, Photo)
- 可包含纯业务计算(不依赖 UI/网络)
- 尽量保持与 UI 无关、与存储无关
-
数据来源与业务用例职责:
-
Repository:屏蔽数据来源(网络/数据库/缓存),对上提供统一接口

- Service/API Client:封装网络请求细节
- UseCase(可选):把“业务动作”封装成可复用单元
- 是否需要 UseCase 取决于项目规模:中大型项目更值得拆出来,小项目可以 VM 直接调用 Repository。
-
-
推荐链路:ViewModel → UseCase → Repository → Domain Model → ViewModel(转UI State)
-

-

小结

数据流向

MVVM架构的优点
- 分离关注点后,代码的可维护性提高
- 由于ViewModel不依赖View,可以在不启动UI的情况下进行单元测试,这使可测试性提高
- 代码更干净,可读性更好
- 单一可信源符合SwiftUI哲学
PokedexUI
README.md
-
PokedexUI implements a Protocol-Oriented MVVM architecture with Clean Architecture principles (不是严格的Clean架构,因为没有Domain层).
-
整洁架构的核心是:
-
把代码按职责分层

-
让依赖方向保持干净可控

-
协议隔离细节

-
Cursor代码解读
- 你可以把 container 理解成:“当前这个 JSON 对象(大括号 {})的读取手柄”。
- KeyedDecodingContainer 就是“对某个 JSON 对象节点的按键读取接口”,支持继续深入嵌套解码
- CodingKey协议的要求会由编码器自动生成,主要得到一个stringValue属性(比如.id.stringValue),会在解码时被间接调用
- Keyed 是“字典/对象”,Unkeyed 是“数组/列表”

- 解码中嵌套类/结构体就是嵌套字典

- 核心是把“怎么请求/怎么把响应变成 ViewModel”交给泛型 APIService 和 Config
- PokemonService 是“业务入口”,APIService 是“通用引擎”
- Config 负责告诉 APIService 三个点:列表请求怎么建、详情请求怎么建、最终怎么把详情数组变成 ViewModel 数组
- @ModelActor 会为这个 actor 提供一个隔离的 SwiftData modelContext
