前段时间将公司的 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设计模式搭建项目基础架构初探大概就结束了,这是篇对自己项目经验的总结,也希望这篇文章能够帮到大家一点点。新年快乐!