原文 : 与佳期的个人博客(gonghonglou.com)
前段时间将公司的 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
自动刷新。
@property (nonatomic, copy) NSArray *dataArray;
在 ViewController
里实现对 dataArray
属性的监听:
@weakify(self);
[RACObserve(self.viewModel, dataArray) subscribeNext:^(id _Nullable x) {
@strongify(self);
[self.viewContainer.tableView reloadData];
}];
方式二:监听消息
如,ViewModel
存在一个属性 dataArray
,但并不对外暴露,而是对外暴露一个 RACSignal
方法,当产生 RACSignal
信号时,View
层的 UITableView
自动刷新。
- (RACSignal *)arraySignal {
return [RACObserve(self, dataArray) filter:^BOOL(id value) {
return YES;
}];
}
在 ViewController
里实现对 arraySignal
信号的监听:
@weakify(self);
[self.viewModel.arraySignal subscribeNext:^(id x) {
@strongify(self);
[self.viewContainer.tableView reloadData];
}];
这两种方式相比,方式一写法简单,但需要对外暴露属性,方式二写法复杂但不需要暴露属性,只对外暴露一个信号方法。
页面跳转
当 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
完成布局,添加登录按钮:
- (instancetype)init {
self = [super init];
if (self) {
self.confirmButton = [UIButton buttonWithType:UIButtonTypeSystem];
self.confirmButton.frame = CGRectMake(100, 300, 175, 40);
self.confirmButton.layer.cornerRadius = 4.0;
self.confirmButton.backgroundColor = [UIColor redColor];
[self.confirmButton setTitle:@"点击登录" forState:UIControlStateNormal];
[self.confirmButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[self addSubview:self.confirmButton];
}
return self;
}
2、ViewController
绑定登录按钮方法:
- (void)clickConfirmButton {
[self.viewModel login];
}
3、ViewModel
处理登录逻辑,操作 MACoordinatingController
进行页面跳转:
- (void)login {
[[MACoordinatingController sharedInstance] pushToHomeViewController];
}
4、MACoordinatingController
执行页面跳转:
- (void)pushToHomeViewController {
if (!self.homeVC) self.homeVC = [MAHomeViewController new];
[_activeViewController.navigationController pushViewController:self.homeVC animated:YES];
}
主页(Home)
1、ViewContainer
完成布局,添加 UITableView
2、ViewController
设置 UITableView
代理并实现代理方法:
// self.viewContainer.tableView.dataSource = self;
// self.viewContainer.tableView.delegate = self;
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.viewModel.dataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MAHomeTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[MAHomeTableViewCell cellIdentifier]];
if (!cell) {
cell = [[MAHomeTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:[MAHomeTableViewCell cellIdentifier]];
}
[cell setHomeTableViewCellWithVO:self.viewModel.dataArray[indexPath.row]];
return cell;
}
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self.viewModel didSelectedCellWithIndexPath:indexPath];
}
并设置 ViewModel
数据监听:
- (void)setObserve {
@weakify(self);
[RACObserve(self.viewModel, dataArray) subscribeNext:^(id _Nullable x) {
@strongify(self);
[self.viewContainer.tableView reloadData];
}];
}
3、ViewModel
进行逻辑处理,生成数据
- (void)operateDataArray {
NSMutableArray *mutableArray = [NSMutableArray new];
for (NSInteger i = 0; i < 10; i++) {
MAHomeTableViewCellVO *cellVO = [MAHomeTableViewCellVO new];
cellVO.title = [NSString stringWithFormat:@"title -- %ld", (long)i];
cellVO.message = [NSString stringWithFormat:@"message -- %ld", (long)i];
[mutableArray addObject:cellVO];
}
self.dataArray = [mutableArray copy];
}
并响应 cell
点击,操作 MACoordinatingController
进行页面跳转
4、MACoordinatingController
执行页面跳转
其中,MAHomeTableViewCell
的内容是由 MAHomeTableViewCellVO
来决定的。
详情页(Detail)
1、ViewContainer
完成布局
2、ViewController
设置 ViewModel
数据监听
3、ViewModel
进行逻辑处理,生成数据
整体流程大概如下图:
关于页面跳转
对于中介者跳转页面代码如:
- (void)pushToHomeViewController {
if (!self.homeVC) self.homeVC = [MAHomeViewController new];
[_activeViewController.navigationController pushViewController:self.homeVC animated:YES];
}
我们需要记录 _activeViewController
的值来对当前页面进行操作,其中处理方法有两种。
方式一:
令所有的 ViewController
继承 BaseViewController
,在 BaseViewController
的 - (void)viewWillAppear:
方法里设置 _activeViewController
:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[MACoordinatingController sharedInstance].activeViewController = self;
}
方式二:
采用第一种方式就需要令所有的 ViewController
继承 BaseViewController
,显示不太友好,我们可以选用 Method Swizzling
的方式替换掉所有的 ViewController
的 - (void)viewWillAppear:
方法,在已替换的 - (void)ma_viewWillAppear:
方法里设置 MACoordinatingController
的 activeViewController
为当前的 ViewController
:
@implementation UIViewController (MAAppear)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(ma_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)ma_viewWillAppear:(BOOL)animated {
[self ma_viewWillAppear:animated];
if (![self isKindOfClass:NSClassFromString(@"UIInputWindowController")]) {
[MACoordinatingController sharedInstance].activeViewController = self;
}
}
@end
关于 Method Swizzling
可以参考我之前的文章:从 SafeKit 看异常保护及 Method Swizzling 使用分析
到这里,采用MVVM设计模式搭建项目基础架构初探大概就结束了,这是篇对自己项目经验的总结,也希望这篇文章能够帮到大家一点点。新年快乐!
附上 MVVMArchitectureDemo GitHub 地址
后记
小白出手,请多指教。如言有误,还望斧正!
转载请保留原文地址:http://gonghonglou.com/2018/02/12/mvvm-architecture/