前段时间将公司的 iOS 移动端项目采用 MVVM 设计模式重构了,说是重构,其实是新开了一个工程按照原工程业务逻辑重写了一遍。重构结束后整个工程清爽了不要太多,逻辑清晰,维护轻松,新功能开发起来效率得到明显提升。重构结束后就打算将项目基础架构总结一下,结合 MVVM 设计模式写份小结,但苦于一直没有时间,以致拖到了现在,马上过年了现在来还账。。。
首先声明的是,本篇文章是对自己过去几个月里重构工作中项目架构方面的总结,因为项目比较轻,所以文章讲的内容更适合一个轻量型工程,至于对复杂工程的适用程度则另当别论,小白经验求轻喷~
对于这篇博客的内容 MVVM 是重点,但不是目的,目的是讲清对一个项目怎样搭建基础架构,所以文章会以 MVVM 设计模式为中心展开讲解整个项目基础架构的搭建,以及在项目中对架构方面自己的处理方法。
MVC 与 MVVM
关于 MVC、MVP、MVVM 各种设计模式的区别及特点网上已经有了一大推的文章去讲,这里就不再详细赘述了,毕竟这不是本篇文章的重点。但我们总归要先认识 MVVM 设计模式嘛,提到 MVVM 就离不开 MVC 的延伸,毕竟 MVVM 设计模式是从 MVC 衍生出来的,请看图:

通过这张图我想尽量将这两种设计模式的不同点区分清楚
MVC设计模式View绑定事件并响应,传递给Controller处理Controller处理逻辑,发起网络请求向Model存储数据,或调用缓存方法向Model获取数据以控制View的展示Model负责数据存储
MVVM设计模式View绑定事件并响应,传递给ViewModel处理ViewModel处理逻辑,发起网络请求向Model存储数据Model负责数据存储Model数据变动,ViewModel会得到响应并进行逻辑处理ViewModel数据变动,View会得到响应并改变页面
文件分类与功能划分
对于每一模块如 Login、Home、Detail 等都有对应的自己的 ViewContainer、ViewController、ViewModel,基本一个模块对应一块页面,文件分区如下:

简单讲一下各个文件的作用:
1.
ViewContainer:- 1.在
MVVM设计模式中与ViewController共同扮演View的角色 - 2.负责页面子控件布局,可接收一个
VO类决定该页面内容所需要的数据模型,对于页面 - 3.对于页面内容所需要的数据可对外暴露一个接口,接收一个包含该页面所需数据的
VO类
- 1.在
2.
ViewController:- 1.在
MVVM设计模式中与ViewContainer共同扮演View的角色 - 2.负责绑定子控件事件、代理,实现绑定事件到
ViewModel、实现代理方法 - 3.响应
ViewModel数据变化以控制View层页面展示
- 1.在
3.
ViewModel:- 1.在
MVVM设计模式中扮演ViewModel的角色 - 2.负责处理
ViewController传递的点击等事件 - 3.负责逻辑处理,业务处理。如:发起网络请求、处理缓存数据、生成
VO类对象 - 4.响应
Model数据变化
- 1.在
以上涉及到额外的两个概念: VO 类和 Model 类:
1.
VO类:- 1.专职于
View层页面展示所需要的数据,提供给View层 - 2.这样
View层就不必关心源数据如何,而仅关心VO类有什么样的数据就展示什么样的页面,将源数据的处理操作交给ViewModel去做 - 3.
VO类既可以给Cell提供为CellVO,也可以给子视图提供为SubViewVO
- 1.专职于
2.
Model类:- 1.在
MVVM设计模式中扮演Model的角色 - 2.基本每个模块都有拥有一份
ViewContainer、ViewController、ViewModel,但并非每个模块都拥有一份Model,所以只给需要Model类的模块添加该类文件 - 3.并非因为该模块没有
Model类文件就说这个模块没有Model层,我们所说的MVVM设计模式是一个抽象的概念,不要被上图中存在的一一对应的物理文件就忽略了工程中的Model层 - 4.我们通常会将整个工程的
Model层抽离出来放在一起,比如网络请求部分、缓存部分、配置文件这些都属于MVVM设计模式里的Model层的内容,而这些内容大都可以抽离成单独的工具类,所以不会存在于每个模块的文件夹分类中 - 5.当然,如果某模块需要一份私有的
Model类文件,我们仍然会为该模块新建Model类文件
- 1.在
双向绑定
MVVM 设计模式有一个最大的特点就是双向绑定,当 ViewModel 中的数据发生变化时,View 层会自动响应数据变化以更改页面,而实现这一功能的方法可以通过 KVO、通知等方式,只不过自己去实现这些功能难免会让 MVVM 的使用变得复杂,而作为函数式响应式编程的大神级作品 ReactiveCocoa 可以完美的帮我们实现这些操作。
网络上关于 MVVM + ReactiveCocoa 工作方式的文章也是一大推,这里也就不详细赘述了,这里用到的 ReactiveCocoa 的功能非常少,我们仅仅实现双向绑定能够响应数据变化即可。
方式一:监听属性
如,ViewModel 存在一个属性 dataArray,当该属性变化时,View 层的 UITableView 自动刷新。
1 | @property (nonatomic, copy) NSArray *dataArray; |
在 ViewController 里实现对 dataArray 属性的监听:
1 |
|
方式二:监听消息
如,ViewModel 存在一个属性 dataArray,但并不对外暴露,而是对外暴露一个 RACSignal 方法,当产生 RACSignal 信号时,View 层的 UITableView 自动刷新。
1 | - (RACSignal *)arraySignal { |
在 ViewController 里实现对 arraySignal 信号的监听:
1 |
|
这两种方式相比,方式一写法简单,但需要对外暴露属性,方式二写法复杂但不需要暴露属性,只对外暴露一个信号方法。
页面跳转
当 View 响应事件并驱动 ViewModel 进行业务逻辑处理涉及到页面变动或跳转时,我们的逻辑应当是这样的:
页面操作流程
对于页面内局部内容的改变,不涉及 push、present 等操作时,页面操作由当前 View 控制,流程如下:

上图为页面操作的流程:
1、View 响应事件并传递给 ViewModel
2、ViewModel 逻辑处理完成后通知 View
3、View 接收 ViewModel 的通知并进行页面操作
页面跳转流程
对于页面跳转,比如 push、present 等操作时,跳转行为由 Mediator 控制,流程如下:

上图为页面跳转的流程:
1、View 响应事件并传递给 ViewModel
2、ViewModel 逻辑处理完成后通知中介者
3、Mediator 中介者操作页面进行跳转
中介者(Mediator)
其中,这里涉及到 中介者(Mediator) 的概念:
为了方便进行页面跳转的操作及避免依赖,我们引入了 中介者(Mediator) 的概念,ViewModel 持有 Mediator,Mediator 控制所有页面的跳转,只对外暴露跳转接口。
这样,ViewModel 并不关心页面跳转的细节,只需调用 Mediator 的方法即可,而页面跳转的实现,甚至是跳转的方向都可以有 Mediator 来决定。

代码实践
我们努力以最简单的例子展现整个架构的流程,涉及到大概三个页面:
1、登录页:点击按钮执行登录操作并推出主页面
2、主页:展示一个 UITableView,并且可点击 UITableViewCell 推出详情页面
3、详情页:展示详情信息
登录页(Login)
1、ViewContainer 完成布局,添加登录按钮:
1 | - (instancetype)init { |
2、ViewController 绑定登录按钮方法:
1 | - (void)clickConfirmButton { |
3、ViewModel 处理登录逻辑,操作 MACoordinatingController 进行页面跳转:
1 | - (void)login { |
4、MACoordinatingController 执行页面跳转:
1 | - (void)pushToHomeViewController { |
主页(Home)
1、ViewContainer 完成布局,添加 UITableView
2、ViewController 设置 UITableView 代理并实现代理方法:
1 | // self.viewContainer.tableView.dataSource = self; |
并设置 ViewModel 数据监听:
1 | - (void)setObserve { |
3、ViewModel 进行逻辑处理,生成数据
1 | - (void)operateDataArray { |
并响应 cell 点击,操作 MACoordinatingController 进行页面跳转
4、MACoordinatingController 执行页面跳转
其中,MAHomeTableViewCell 的内容是由 MAHomeTableViewCellVO 来决定的。
详情页(Detail)
1、ViewContainer 完成布局
2、ViewController 设置 ViewModel 数据监听
3、ViewModel 进行逻辑处理,生成数据
整体流程大概如下图:

关于页面跳转
对于中介者跳转页面代码如:
1 | - (void)pushToHomeViewController { |
我们需要记录 _activeViewController 的值来对当前页面进行操作,其中处理方法有两种。
方式一:
令所有的 ViewController 继承 BaseViewController,在 BaseViewController 的 - (void)viewWillAppear: 方法里设置 _activeViewController:
1 | - (void)viewWillAppear:(BOOL)animated { |
方式二:
采用第一种方式就需要令所有的 ViewController 继承 BaseViewController,显示不太友好,我们可以选用 Method Swizzling 的方式替换掉所有的 ViewController 的 - (void)viewWillAppear: 方法,在已替换的 - (void)ma_viewWillAppear: 方法里设置 MACoordinatingController 的 activeViewController 为当前的 ViewController:
1 | @implementation UIViewController (MAAppear) |
关于 Method Swizzling 可以参考我之前的文章:从 SafeKit 看异常保护及 Method Swizzling 使用分析
到这里,采用MVVM设计模式搭建项目基础架构初探大概就结束了,这是篇对自己项目经验的总结,也希望这篇文章能够帮到大家一点点。新年快乐!