MVX是什么?
MVC、MVVM、MVP...
模型层和视图层职责非常清楚,一个用于处理本地数据的获取以及存储,另一个用于展示内容、接受用户的操作与事件。在这种情况下,应用中的其他功能和逻辑就会被自认而然的扔到X层中。
这个X在MVC中就是Controller层、在MVVM中就是ViewModel层、在MVP中就是Presenter层。这里介绍的时MVC中的控制器层Controller。
下面是iOS中对MVC的隔层交互的最简单的说明:
MVC视图层和模型层分开,由Controller层协调。视图接受用户行为,Controller处理用户行为,更新模型,模型变更通知Controller,Controller更新视图。
总的来说,Controller层要负责以下问题:
- 管理根视图的声明周期和应用声明周期;
- 负责将视图层的View对象添加到根视图上;
- 负责处理用户行为,比如UIButton的点击及手势的触发;
- 储存当前界面状态;
- 处理界面之间的跳转
- 作为UITableView以及其他容器视图的代理
- 负责HTTP请求的发起;
除了上述职责之外,UIViewController对象还可能需要处理业务逻辑以及各种复杂的动画。这也就是为什么在iOS应用中的Controller层都非常庞大、臃肿的原因了,而MVVM、MVP等架构模式的目的之一就是减少Controller中的代码。
管理声明周期
Controller层作为整个MVC架构模式的中枢,承担着非常重要的职责,不仅要与Model以及View层进行交互,还有通过APPDelegate与诸多的应用声明周期打交道。
虽然应用声明周期沟通的工作不在单独的Controller中,但是程序启动后有个根视图控制器作为整个应用的入口self.window.rootController
,还是需要在AppDelegate中进行设置。
除此之外,每一个UIViewController都持有一个视图对象,所以每个视图控制器都要负责这个根视图的加载、布局以及生命周期的管理,包括:
- (void)loadView;
- (void)viewWillLayoutSubviews;
- (void)viewDidLayoutSubviews;
- (void)viewDidLoad;
- (void)viewWillAppear:(BOOL)animated;
- (void)viewDidAppear:(BOOL)animated;
除了负责应用生命周期和视图生命周期,控制器还要负责展示内容和布局。
负责展示内容和布局
因为每一个UIViewController都持有一个UIView的对象作为根视图,所有的视图层对象要想出现在屏幕上,都得成为这个根视图的子视图,也就是说,视图层完全没办法脱离UIViewController单独存在。一方面就是这个原因,UIViewController的设计导致了所有的视图必须加在根视图上才能工作,另一个方面是控制器隐式承担了应用中路由的工作,处理页面之间的跳转。
用户行为处理
在UIViewController中处理用户行为是经常需要做的事情,这部分代码不能放到视图层或者其他地方的原因是:用户的行为经常要与Controller的上下文有联系,比如页面跳转需要依赖于UINavigationController对象,有的用户行为需要改变模型层的对象、持久存储数据库中的数据或者发出网络请求,主要是我们要秉承MVC的设计理念,避免Model层和View层的直接耦合。
存储当前界面的状态
比如下拉刷新和上拉加载更多。这时就需要在Controller层保存当前显示的页码:
@interface TableViewController ()
@property (nonatomic, assign) NSUInteger currentPage;
@end
只有保存在了当前页数的状态,才能在下次请求网络数据时传入合适的页数,最后获得正确的资源。
在 MVC 的设计中,这种保存当前页面状态的需求是存在的,在很多复杂的页面中,我们也需要维护大量的状态,这也是 Controller 需要承担的重要职责之一。
处理页面之间的跳转
由于 Cocoa Touch 提供了 UINavigationController
和 UITabBarController
这两种容器 Controller,所以 iOS 中界面跳转的这一职责大部分都落到了 Controller 上了。
iOS总共有三种界面跳转方式:
-
UINavigationController 中使用push和pop改变栈顶的UIViewController对象;
-
UITabBarController中点击各个UITabBarItem实现跳转;
-
使用所有的UIViewController实例都具有
-presentViewController:animated:completion
方法;因为所有的UIViewController实例都可以通过navigationController这一属性获取最近的UINavigationController,我们不可避免要在Controller层对界面之间进行跳转操作。
作为源数据以及代理
很多Cocoa Touch 中视图层都是以代理的形式为外界提供接口的,最典型的就是UITableView和它的数据源协议和代理(UITableViewDataSource和UITableViewDelegate)
这是因为UITableView作为视图层,需要根据Model才能自己应该展示什么内容,所以早期的视图层组件都是使用了代理的形式,从Controller或者其他地方获取需要展示的数据。
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.models.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
Model *model = self.models[indexPath.row];
[cell setupWithModel:model];
return cell;
}
上面就是使用 UITableView
时经常需要的方法。
很多文章中都提供了一种用于减少 Controller 层中代理方法数量的技巧,就是使用一个单独的类作为 UITableView
或者其他视图的代理:
self.tableView.delegate = anotherObject;
self.tableView.dataSource = anotherObject;
然而在笔者看来这种办法并没有什么太大的用处,只是将代理方法挪到了一个其他的地方,如果这个代理方法还依赖于当前 UIViewController
实例的上下文,还要向这个对象中传入更多的对象,反而让原有的 MVC 变得更加复杂了。
负责HTTP请求的发起
当用户的行为触发一些事件,比如下拉刷新、更新Model的属性等等,Controller就需要通过Model层提供的接口向服务端发出HTTP请求,这一过程非常简单,但仍然是Controller层的职责,响应用户事件,并更新Model层数据。
iOS 中 Controller 层的职责一直都逃不开与 View 层和 Model 层的交互,因为其作用就是视图层的用户行为进行处理并更新视图的内容,同时也会改变模型层中的数据、使用 HTTP 请求向服务端请求新的数据等作用,其功能就是处理整个应用中的业务逻辑和规则。
几点建议:
1. 不要把 DataSource 提取出来
iOS 中的 UITableView
和 UICollectionView
等需要 dataSource
的视图对象十分常见,在一些文章中会提议将数据源的实现单独放到一个对象中。并没有起到实质性效果,只是简单的将视图控制器中的一部分代码移到了别的位置而已,还会因为增加了额外的类使 Controller 的维护变得更加的复杂。
2. 把业务逻辑移到Model层
控制器中有很多代码和逻辑其实与控制器本身并没有太多的关系,比如:
@implementation ViewController
- (NSString *)formattedPostCreatedAt {
NSDateFormatter *format = [[NSDateFormatter alloc] init];
[format setDateFormat:@"MMM dd, yyyy HH:mm"];
return [format stringFromDate:self.post.createdAt];
}
@end
上述逻辑其实应该属于 Model 层,作为 Post
的一个实例方法.
3. 把视图层代码移到 View 层
Controller 和 View 层是强耦合的,每一个 UIViewController
都会持有一个 UIView
视图对象,这也是导致我们将很多的视图层代码直接放在 Controller 层的原因。
当视图层的视图对象非常多的时候,大量的配置和布局代码就会在控制器中占据大量的位置,我们可以将整个视图层的代码都移到一个单独的 UIView
子类中。
// RegisterView.h
@interface RegisterView : UIView
@property (nonatomic, strong) UITextField *phoneNumberTextField;
@property (nonatomic, strong) UITextField *passwordTextField;
@end
// RegisterView.m
@implementation RegisterView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self addSubview:self.phoneNumberTextField];
[self addSubview:self.passwordTextField];
[self.phoneNumberTextField mas_makeConstraints:^(MASConstraintMaker *make) {
...
}];
[self.passwordTextField mas_makeConstraints:^(MASConstraintMaker *make) {
...
}];
}
return self;
}
- (UITextField *)phoneNumberTextField {
if (!_phoneNumberTextField) {
_phoneNumberTextField = [[UITextField alloc] init];
_phoneNumberTextField.font = [UIFont systemFontOfSize:16];
}
return _phoneNumberTextField;
}
- (UITextField *)passwordTextField {
if (!_passwordTextField) {
_passwordTextField = [[UITextField alloc] init];
...
}
return _passwordTextField;
}
@end
而 Controller 需要持有该视图对象,并将自己持有的根视图替换成该视图对象:
@interface ViewController ()
@property (nonatomic, strong) RegisterView *view;
@end
@implementation ViewController
@dynamic view;
- (void)loadView {
self.view = [[RegisterView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
}
- (void)viewDidLoad {
[super viewDidLoad];
}
@end
在UIVIewController中,我们可以重写loadView
改变其本身持有的视图对象,并使用的新的@property声明以及@dynamic改变Controller持有的根视图,这样我们就把视图层的配置和布局代码从控制器中完全分离了。
4. 使用pragma 或 extension 分割代码块
将具有相同功能的代码分块并使用 pragma
预编译指定(oc)或者 MARK
加上 extension
(swift)对代码块进行分割。
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 100.0;
}
...
class ViewController: UIViewController {}
// MARK: - UI
extension ViewController {}
// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource {}
// MARK: - UITableViewDelegate
extension ViewController: UITableViewDelegate {}
// MARK: - Callback
extension ViewController {}
// MARK: - Getter/Setter
extension ViewController {}
// MARK: - Helper
extension ViewController {}
一个UIVIewController大体上由这些部分组成:
- 声明周期及一些需要重写的方法
- 视图层代码的初始化
- 各种数据源和代理协议的实现
- 事件、手势和通知的回调
- 实例变量的存取方法
- 一些其他的Helper方法
5.耦合的View和Model层
很多iOS项目中都会给UIView添加一个绑定Model对象的方法,比如说:
@implementation UIView (Model)
- (void)setupWithModel:(id)model{}
@end
这个方法也可能叫做 -bindWithModel:
或者其他名字,其作用就是根据传入的 Model 对象更新当前是视图中的各种状态,比如 UILabel
中的文本、UIImageView
中的图片等等。
有了上述分类,我们可以再任意的 UIView
的子类中覆写该方法:
- (void)setupWithModel:(Model *)model {
self.imageView.image = model.image;
self.label.text = model.name;
}
这种做法其实是将原本Controller做的事情放到了View中,由视图层来负责如何展示模型对象,虽然它能减少Controller中的代码,但是也导致了View和Model的耦合,这样的设计不太符合MVC的架构,这样的视图依赖于外部的模型对象,如果同一个视图需要展示多种类型的模型时就会遇到问题。
根据以上的分析,由于 Controller 在 MVC 中所处的位置,如果不脱离 MVC 架构模式,那么 Controller 的职责很难简化,只能在代码规范和职责划分上进行限制,因此我们需要看下由MVC演化出来的MVP和MVVM到底是什么,有何差异。
网友评论