在前两篇文章中,我们已经对 iOS 中的 Model 层以及 View 层进行了分析,划分出了它们的具体职责,其中 Model 层除了负责数据的持久存储、缓存工作,还要负责所有 HTTP 请求的发出等工作;而对于 View 层的职责,我们并没有做出太多的改变,有的只是细分其内部的视图种类,以及分离 UIView 不应该具有的属性和功能。
如果想要具体了解笔者对 Model 层以及 View 层的理解和设计,这是前面两篇文章的链接:《谈谈 MVX 中的 Model 层》、《谈谈 MVX 中的 View 层》
这是 MVX 系列的第三篇文章,而这篇文章准备介绍整个 MVX 中无法避免的话题,也就是 X 这一部分。
X 是什么
在进入正题之前,我们首先要知道这里的 X 到底是什么?无论是在 iOS 开发领域还是其它的领域,造出了一堆又一堆的名词,除了我们最常见的 MVC 和 MVVM 以及 Android 中的 MVP 还有一些其他的奇奇怪怪的名词。

模型层和视图层是整个客户端应用不可分割的一部分,它们的职责非常清楚,一个用于处理本地数据的获取以及存储,另一个用于展示内容、接受用户的操作与事件;在这种情况下,整个应用中的其它功能和逻辑就会被自然而然的扔到 X 层中。
这个 X 在 MVC 中就是 Controller 层、在 MVVM 中就是 ViewModel 层,而在 MVP中就是 Presenter 层,这篇文章介绍的就是 MVC 中的控制器层 Controller。
臃肿的 Controller
从 Cocoa Touch 框架使用十年以来,iOS 开发者就一直遵循框架中的设计,使用 Model-View-Controller 的架构模式开发 iOS 应用程序,下面也是对 iOS 中 MVC 的各层交互的最简单的说明。

iOS 中的 Model 层大多为 NSObject 的子类,也就是一个简单的对象;所有的 View 层对象都是 UIView 的子类;而 Controller 层的对象都是 UIViewController 的实例。
我们在这一节中主要是介绍 UIViewController 作为 Controller 层中的最重要的对象,它具有哪些职责,它与 Model 以及 View 层是如何进行交互的。
总体来说,Controller 层要负责以下的问题(包括但不仅限于):
- 管理根视图的生命周期和应用生命周期
- 负责将视图层的 UIView 对象添加到持有的根视图上;
- 负责处理用户行为,比如 UIButton 的点击以及手势的触发;
- 储存当前界面的状态;
- 处理界面之间的跳转;
- 作为 UITableView 以及其它容器视图的代理以及数据源;
- 负责 HTTP 请求的发起;
除了上述职责外,UIViewController 对象还可能需要处理业务逻辑以及各种复杂的动画,这也就是为什么在 iOS 应用中的 Controller 层都非常庞大、臃肿的原因了,而 MVVM、MVP 等架构模式的目的之一就是减少单一 Controller 中的代码。
管理生命周期
Controller 层作为整个 MVC 架构模式的中枢,承担着非常重要的职责,不仅要与 Model 以及 View 层进行交互,还有通过 AppDelegate 与诸多的应用生命周期打交道。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions;
- (void)applicationWillResignActive:(UIApplication *)application;
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler;
虽然与应用生命周期沟通的工作并不在单独的 Controller 中,但是 self.window.rootController 作为整个应用程序界面的入口,还是需要在 AppDelegate 中进行设置。
除此之外,由于每一个 UIViewController 都持有一个视图对象,所以每一个 UIViewController 都需要负责这个根视图的加载、布局以及生命周期的管理,包括:
- (void)loadView;
- (void)viewWillLayoutSubviews;
- (void)viewDidLayoutSubviews;
- (void)viewDidLoad;
- (void)viewWillAppear:(BOOL)animated;
- (void)viewDidAppear:(BOOL)animated;
除了负责应用生命周期和视图生命周期,控制器还要负责展示内容和布局。
负责展示内容和布局
由于每一个 UIViewController 都持有一个 UIView 的对象,所以视图层的对象想要出现在屏幕上,必须成为这个根视图的子视图,也就是说视图层完全没有办法脱离 UIViewController 而单独存在,其一方面是因为 UIViewController 隐式的承担了应用中路由的工作,处理界面之间的跳转,另一方面就是 UIViewController 的设计导致了所有的视图必须加在其根视图上才能工作。

我们来看一段 UIViewController 中关于视图层的简单代码:
- (void)viewDidLoad {
[super viewDidLoad];
[self setupUI];
}
- (void)setupUI {
_backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"backgroundView"]];
_registerButton = [[UIButton alloc] init];
[_registerButton setTitle:@"注册" forState:UIControlStateNormal];
[_registerButton setTitleColor:UIColorFromRGB(0x00C3F3) forState:UIControlStateNormal];
[_registerButton addTarget:self action:@selector(registerButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_backgroundView];
[self.view addSubview:_registerButton];
[_backgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.view);
}];
[_registerButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(140, 45));
make.bottom.mas_equalTo(self.view).offset(-25);
make.left.mas_equalTo(self.view).offset(32);
}];
}
在这个欢迎界面以及大多数界面中,由于视图层的代码非常简单,我们很多情况下并不会去写一个单独的 UIView 类,而是将全部的视图层代码丢到了 UIViewController 中,这种情况下甚至也没有 Model 层,Controller 承担了全部的工作。

上述的代码对视图进行了初始化,将需要展示的视图加到了自己持有的根视图中,然后对这些视图进行简单的布局。
当然我们也可以将视图的初始化单独放到一个类中,不过仍然需要处理 DRKBackgroundView 视图的布局等问题。
- (void)setupUI {
DRKBackgroundView *backgroundView = [[DRKBackgroundView alloc] init];
[backgroundView.registerButton addTarget:self action:@selector(registerButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:backgroundView];
[backgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.view);
}];
}
UIViewController 的这种中心化的设计虽然简单,不过也导致了很多代码没有办法真正解耦,视图层必须依赖于 UIViewController 才能展示。
惰性初始化
当然,很多人在 Controller 中也会使用惰性初始化的方式生成 Controller 中使用的视图,比如:
@interface ViewController ()
@property (nonatomic, strong) UIImageView *backgroundView;
@end
@implementation ViewController
- (UIImageView *)backgroundView {
if (!_backgroundView) {
_backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"backgroundView"]];
}
return _backgroundView;
}
@end
这样在 -viewDidLoad 方法中就可以直接处理视图的视图层级以及布局工作:
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.backgroundView];
[self.backgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.view);
}];
}
惰性初始化的方法与其他方法其实并没有什么绝对的优劣,两者的选择只是对于代码规范的一种选择,我们所需要做的,只是在同一个项目中将其中一种做法坚持到底。
处理用户行为
在 UIViewController 中处理用户的行为是经常需要做的事情,这部分代码不能放到视图层或者其他地方的原因是,用户的行为经常需要与 Controller 的上下文有联系,比如,界面的跳转需要依赖于 UINavigationController 对象:
- (void)registerButtonTapped:(UIButton *)button {
RegisterViewController *registerViewController = [[RegisterViewController alloc] init];
[self.navigationController pushViewController:registerViewController animated:YES];
}
而有的用户行为需要改变模型层的对象、持久存储数据库中的数据或者发出网络请求,主要因为我们要秉承着 MVC 的设计理念,避免 Model 层和 View 层的直接耦合。
存储当前界面的状态
在 iOS 中,我们经常需要处理表视图,而在现有的大部分表视图在加载内容时都会进行分页,使用下拉刷新和上拉加载的方式获取新的条目,而这就需要在 Controller 层保存当前显示的页数:
@interface TableViewController ()
@property (nonatomic, assign) NSUInteger currentPage;
@end
只有保存在了当前页数的状态,才能在下次请求网络数据时传入合适的页数,最后获得正确的资源,当然哪怕当前页数是可以计算出来的,比如通过当前的 Model 对象的数和每页个 Model 数,在这种情况下,我们也需要在当前 Controller 中 Model 数组的值。
@interface TableViewController ()
@property (nonatomic, strong) NSArray<Model *> *models;
@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 层对界面之间的跳转进行操作。
当然,我们也可以引入 Router 路由对 UIViewController 进行注册,在访问合适的 URL 时,通过根 UINavigationController 进行跳转,不过这不是本篇文章想要说明的内容。
UINavigationController 提供的 API 还是非常简单的,我们可以直接使用 -pushViewController:animated: 就可以进行跳转。
RegisterViewController *registerViewController = [[RegisterViewController alloc] init];
[self.navigationController pushViewController:registerViewController animated:YES];
作为数据源以及代理
很多 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 层的数据。
- (void)registerButtonTapped:(UIButton *)button {
LoginManager *manager = [LoginManager manager];
manager.countryCode = _registerPanelView.countryCode;
...
[manager startWithSuccessHandler:^(CCStudent *user) {
self.currentUser = user;
...
} failureHandler:^(NSError *error) {
...
}];
}
当按钮被点击时 LoginManager 就会执行 -startWithSuccessHandler:failureHandler: 方法发起请求,并在请求结束后执行回调,更新 Model 的数据。
小结
iOS 中 Controller 层的职责一直都逃不开与 View 层和 Model 层的交互,因为其作用就是视图层的用户行为进行处理并更新视图的内容,同时也会改变模型层中的数据、使用 HTTP 请求向服务端请求新的数据等作用,其功能就是处理整个应用中的业务逻辑和规则。
但是由于 iOS 中 Controller 的众多职责,单一的 UIViewController 类可能会有上千行的代码,使得非常难以管理和维护,我们也希望在 iOS 中引入新的架构模式来改变 Controller 过于臃肿这一现状。
几点建议
Controller 层作为 iOS 应用中重要的组成部分,在 MVC 以及类似的架构下,笔者对于 Controller 的设计其实没有太多立竿见影的想法。作为应用中处理绝大多数逻辑的 Controller 其实很难简化其中代码的数量;我们能够做的,也是只对其中的代码进行一定的规范以提高它的可维护性,在这里,笔者有几点对于 Controller 层如何设计的建议,供各位读者参考。
不要把 DataSource 提取出来
iOS 中的 UITableView 和 UICollectionView 等需要 dataSource 的视图对象十分常见,在一些文章中会提议将数据源的实现单独放到一个对象中。
```
void (^configureCell)(PhotoCell, Photo) = ^(PhotoCell* cell, Photo* photo) {
cell.label.text = photo.name;
};
photosArrayDataSource = [[ArrayDataSource alloc] initWithItems:photos
在前两篇文章中,我们已经对 iOS 中的 Model 层以及 View 层进行了分析,划分出了它们的具体职责,其中 Model 层除了负责数据的持久存储、缓存工作,还要负责所有 HTTP 请求的发出等工作;而对于 View 层的职责,我们并没有做出太多的改变,有的只是细分其内部的视图种类,以及分离 UIView 不应该具有的属性和功能。
如果想要具体了解笔者对 Model 层以及 View 层的理解和设计,这是前面两篇文章的链接:《谈谈 MVX 中的 Model 层》、《谈谈 MVX 中的 View 层》
这是 MVX 系列的第三篇文章,而这篇文章准备介绍整个 MVX 中无法避免的话题,也就是 X 这一部分。
X 是什么
在进入正题之前,我们首先要知道这里的 X 到底是什么?无论是在 iOS 开发领域还是其它的领域,造出了一堆又一堆的名词,除了我们最常见的 MVC 和 MVVM 以及 Android 中的 MVP 还有一些其他的奇奇怪怪的名词。

模型层和视图层是整个客户端应用不可分割的一部分,它们的职责非常清楚,一个用于处理本地数据的获取以及存储,另一个用于展示内容、接受用户的操作与事件;在这种情况下,整个应用中的其它功能和逻辑就会被自然而然的扔到 X 层中。
这个 X 在 MVC 中就是 Controller 层、在 MVVM 中就是 ViewModel 层,而在 MVP中就是 Presenter 层,这篇文章介绍的就是 MVC 中的控制器层 Controller。
臃肿的 Controller
从 Cocoa Touch 框架使用十年以来,iOS 开发者就一直遵循框架中的设计,使用 Model-View-Controller 的架构模式开发 iOS 应用程序,下面也是对 iOS 中 MVC 的各层交互的最简单的说明。

iOS 中的 Model 层大多为 NSObject 的子类,也就是一个简单的对象;所有的 View 层对象都是 UIView 的子类;而 Controller 层的对象都是 UIViewController 的实例。
我们在这一节中主要是介绍 UIViewController 作为 Controller 层中的最重要的对象,它具有哪些职责,它与 Model 以及 View 层是如何进行交互的。
总体来说,Controller 层要负责以下的问题(包括但不仅限于):
- 管理根视图的生命周期和应用生命周期
- 负责将视图层的 UIView 对象添加到持有的根视图上;
- 负责处理用户行为,比如 UIButton 的点击以及手势的触发;
- 储存当前界面的状态;
- 处理界面之间的跳转;
- 作为 UITableView 以及其它容器视图的代理以及数据源;
- 负责 HTTP 请求的发起;
除了上述职责外,UIViewController 对象还可能需要处理业务逻辑以及各种复杂的动画,这也就是为什么在 iOS 应用中的 Controller 层都非常庞大、臃肿的原因了,而 MVVM、MVP 等架构模式的目的之一就是减少单一 Controller 中的代码。
管理生命周期
Controller 层作为整个 MVC 架构模式的中枢,承担着非常重要的职责,不仅要与 Model 以及 View 层进行交互,还有通过 AppDelegate 与诸多的应用生命周期打交道。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions;
- (void)applicationWillResignActive:(UIApplication *)application;
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler;
虽然与应用生命周期沟通的工作并不在单独的 Controller 中,但是 self.window.rootController 作为整个应用程序界面的入口,还是需要在 AppDelegate 中进行设置。
除此之外,由于每一个 UIViewController 都持有一个视图对象,所以每一个 UIViewController 都需要负责这个根视图的加载、布局以及生命周期的管理,包括:
- (void)loadView;
- (void)viewWillLayoutSubviews;
- (void)viewDidLayoutSubviews;
- (void)viewDidLoad;
- (void)viewWillAppear:(BOOL)animated;
- (void)viewDidAppear:(BOOL)animated;
除了负责应用生命周期和视图生命周期,控制器还要负责展示内容和布局。
负责展示内容和布局
由于每一个 UIViewController 都持有一个 UIView 的对象,所以视图层的对象想要出现在屏幕上,必须成为这个根视图的子视图,也就是说视图层完全没有办法脱离 UIViewController 而单独存在,其一方面是因为 UIViewController 隐式的承担了应用中路由的工作,处理界面之间的跳转,另一方面就是 UIViewController 的设计导致了所有的视图必须加在其根视图上才能工作。

我们来看一段 UIViewController 中关于视图层的简单代码:
- (void)viewDidLoad {
[super viewDidLoad];
[self setupUI];
}
- (void)setupUI {
_backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"backgroundView"]];
_registerButton = [[UIButton alloc] init];
[_registerButton setTitle:@"注册" forState:UIControlStateNormal];
[_registerButton setTitleColor:UIColorFromRGB(0x00C3F3) forState:UIControlStateNormal];
[_registerButton addTarget:self action:@selector(registerButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_backgroundView];
[self.view addSubview:_registerButton];
[_backgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.view);
}];
[_registerButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(140, 45));
make.bottom.mas_equalTo(self.view).offset(-25);
make.left.mas_equalTo(self.view).offset(32);
}];
}
在这个欢迎界面以及大多数界面中,由于视图层的代码非常简单,我们很多情况下并不会去写一个单独的 UIView 类,而是将全部的视图层代码丢到了 UIViewController 中,这种情况下甚至也没有 Model 层,Controller 承担了全部的工作。

上述的代码对视图进行了初始化,将需要展示的视图加到了自己持有的根视图中,然后对这些视图进行简单的布局。
当然我们也可以将视图的初始化单独放到一个类中,不过仍然需要处理 DRKBackgroundView 视图的布局等问题。
- (void)setupUI {
DRKBackgroundView *backgroundView = [[DRKBackgroundView alloc] init];
[backgroundView.registerButton addTarget:self action:@selector(registerButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:backgroundView];
[backgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.view);
}];
}
UIViewController 的这种中心化的设计虽然简单,不过也导致了很多代码没有办法真正解耦,视图层必须依赖于 UIViewController 才能展示。
惰性初始化
当然,很多人在 Controller 中也会使用惰性初始化的方式生成 Controller 中使用的视图,比如:
@interface ViewController ()
@property (nonatomic, strong) UIImageView *backgroundView;
@end
@implementation ViewController
- (UIImageView *)backgroundView {
if (!_backgroundView) {
_backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"backgroundView"]];
}
return _backgroundView;
}
@end
这样在 -viewDidLoad 方法中就可以直接处理视图的视图层级以及布局工作:
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.backgroundView];
[self.backgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.view);
}];
}
惰性初始化的方法与其他方法其实并没有什么绝对的优劣,两者的选择只是对于代码规范的一种选择,我们所需要做的,只是在同一个项目中将其中一种做法坚持到底。
处理用户行为
在 UIViewController 中处理用户的行为是经常需要做的事情,这部分代码不能放到视图层或者其他地方的原因是,用户的行为经常需要与 Controller 的上下文有联系,比如,界面的跳转需要依赖于 UINavigationController 对象:
- (void)registerButtonTapped:(UIButton *)button {
RegisterViewController *registerViewController = [[RegisterViewController alloc] init];
[self.navigationController pushViewController:registerViewController animated:YES];
}
而有的用户行为需要改变模型层的对象、持久存储数据库中的数据或者发出网络请求,主要因为我们要秉承着 MVC 的设计理念,避免 Model 层和 View 层的直接耦合。
存储当前界面的状态
在 iOS 中,我们经常需要处理表视图,而在现有的大部分表视图在加载内容时都会进行分页,使用下拉刷新和上拉加载的方式获取新的条目,而这就需要在 Controller 层保存当前显示的页数:
@interface TableViewController ()
@property (nonatomic, assign) NSUInteger currentPage;
@end
只有保存在了当前页数的状态,才能在下次请求网络数据时传入合适的页数,最后获得正确的资源,当然哪怕当前页数是可以计算出来的,比如通过当前的 Model 对象的数和每页个 Model 数,在这种情况下,我们也需要在当前 Controller 中 Model 数组的值。
@interface TableViewController ()
@property (nonatomic, strong) NSArray<Model *> *models;
@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 层对界面之间的跳转进行操作。
当然,我们也可以引入 Router 路由对 UIViewController 进行注册,在访问合适的 URL 时,通过根 UINavigationController 进行跳转,不过这不是本篇文章想要说明的内容。
UINavigationController 提供的 API 还是非常简单的,我们可以直接使用 -pushViewController:animated: 就可以进行跳转。
RegisterViewController *registerViewController = [[RegisterViewController alloc] init];
[self.navigationController pushViewController:registerViewController animated:YES];
作为数据源以及代理
很多 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 层的数据。
- (void)registerButtonTapped:(UIButton *)button {
LoginManager *manager = [LoginManager manager];
manager.countryCode = _registerPanelView.countryCode;
...
[manager startWithSuccessHandler:^(CCStudent *user) {
self.currentUser = user;
...
} failureHandler:^(NSError *error) {
...
}];
}
当按钮被点击时 LoginManager 就会执行 -startWithSuccessHandler:failureHandler: 方法发起请求,并在请求结束后执行回调,更新 Model 的数据。
小结
iOS 中 Controller 层的职责一直都逃不开与 View 层和 Model 层的交互,因为其作用就是视图层的用户行为进行处理并更新视图的内容,同时也会改变模型层中的数据、使用 HTTP 请求向服务端请求新的数据等作用,其功能就是处理整个应用中的业务逻辑和规则。
但是由于 iOS 中 Controller 的众多职责,单一的 UIViewController 类可能会有上千行的代码,使得非常难以管理和维护,我们也希望在 iOS 中引入新的架构模式来改变 Controller 过于臃肿这一现状。
几点建议
Controller 层作为 iOS 应用中重要的组成部分,在 MVC 以及类似的架构下,笔者对于 Controller 的设计其实没有太多立竿见影的想法。作为应用中处理绝大多数逻辑的 Controller 其实很难简化其中代码的数量;我们能够做的,也是只对其中的代码进行一定的规范以提高它的可维护性,在这里,笔者有几点对于 Controller 层如何设计的建议,供各位读者参考。
不要把 DataSource 提取出来
iOS 中的 UITableView 和 UICollectionView 等需要 dataSource 的视图对象十分常见,在一些文章中会提议将数据源的实现单独放到一个对象中。
```
void (^configureCell)(PhotoCell, Photo) = ^(PhotoCell* cell, Photo* photo) {
cell.label.text = photo.name;
};
photosArrayDataSource = [[ArrayDataSource alloc] initWithItems:photos
在前两篇文章中,我们已经对 iOS 中的 Model 层以及 View 层进行了分析,划分出了它们的具体职责,其中 Model 层除了负责数据的持久存储、缓存工作,还要负责所有 HTTP 请求的发出等工作;而对于 View 层的职责,我们并没有做出太多的改变,有的只是细分其内部的视图种类,以及分离 UIView 不应该具有的属性和功能。
如果想要具体了解笔者对 Model 层以及 View 层的理解和设计,这是前面两篇文章的链接:《谈谈 MVX 中的 Model 层》、《谈谈 MVX 中的 View 层》
这是 MVX 系列的第三篇文章,而这篇文章准备介绍整个 MVX 中无法避免的话题,也就是 X 这一部分。
X 是什么
在进入正题之前,我们首先要知道这里的 X 到底是什么?无论是在 iOS 开发领域还是其它的领域,造出了一堆又一堆的名词,除了我们最常见的 MVC 和 MVVM 以及 Android 中的 MVP 还有一些其他的奇奇怪怪的名词。

模型层和视图层是整个客户端应用不可分割的一部分,它们的职责非常清楚,一个用于处理本地数据的获取以及存储,另一个用于展示内容、接受用户的操作与事件;在这种情况下,整个应用中的其它功能和逻辑就会被自然而然的扔到 X 层中。
这个 X 在 MVC 中就是 Controller 层、在 MVVM 中就是 ViewModel 层,而在 MVP中就是 Presenter 层,这篇文章介绍的就是 MVC 中的控制器层 Controller。
臃肿的 Controller
从 Cocoa Touch 框架使用十年以来,iOS 开发者就一直遵循框架中的设计,使用 Model-View-Controller 的架构模式开发 iOS 应用程序,下面也是对 iOS 中 MVC 的各层交互的最简单的说明。

iOS 中的 Model 层大多为 NSObject 的子类,也就是一个简单的对象;所有的 View 层对象都是 UIView 的子类;而 Controller 层的对象都是 UIViewController 的实例。
我们在这一节中主要是介绍 UIViewController 作为 Controller 层中的最重要的对象,它具有哪些职责,它与 Model 以及 View 层是如何进行交互的。
总体来说,Controller 层要负责以下的问题(包括但不仅限于):
- 管理根视图的生命周期和应用生命周期
- 负责将视图层的 UIView 对象添加到持有的根视图上;
- 负责处理用户行为,比如 UIButton 的点击以及手势的触发;
- 储存当前界面的状态;
- 处理界面之间的跳转;
- 作为 UITableView 以及其它容器视图的代理以及数据源;
- 负责 HTTP 请求的发起;
除了上述职责外,UIViewController 对象还可能需要处理业务逻辑以及各种复杂的动画,这也就是为什么在 iOS 应用中的 Controller 层都非常庞大、臃肿的原因了,而 MVVM、MVP 等架构模式的目的之一就是减少单一 Controller 中的代码。
管理生命周期
Controller 层作为整个 MVC 架构模式的中枢,承担着非常重要的职责,不仅要与 Model 以及 View 层进行交互,还有通过 AppDelegate 与诸多的应用生命周期打交道。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions;
- (void)applicationWillResignActive:(UIApplication *)application;
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler;
虽然与应用生命周期沟通的工作并不在单独的 Controller 中,但是 self.window.rootController 作为整个应用程序界面的入口,还是需要在 AppDelegate 中进行设置。
除此之外,由于每一个 UIViewController 都持有一个视图对象,所以每一个 UIViewController 都需要负责这个根视图的加载、布局以及生命周期的管理,包括:
- (void)loadView;
- (void)viewWillLayoutSubviews;
- (void)viewDidLayoutSubviews;
- (void)viewDidLoad;
- (void)viewWillAppear:(BOOL)animated;
- (void)viewDidAppear:(BOOL)animated;
除了负责应用生命周期和视图生命周期,控制器还要负责展示内容和布局。
负责展示内容和布局
由于每一个 UIViewController 都持有一个 UIView 的对象,所以视图层的对象想要出现在屏幕上,必须成为这个根视图的子视图,也就是说视图层完全没有办法脱离 UIViewController 而单独存在,其一方面是因为 UIViewController 隐式的承担了应用中路由的工作,处理界面之间的跳转,另一方面就是 UIViewController 的设计导致了所有的视图必须加在其根视图上才能工作。

我们来看一段 UIViewController 中关于视图层的简单代码:
- (void)viewDidLoad {
[super viewDidLoad];
[self setupUI];
}
- (void)setupUI {
_backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"backgroundView"]];
_registerButton = [[UIButton alloc] init];
[_registerButton setTitle:@"注册" forState:UIControlStateNormal];
[_registerButton setTitleColor:UIColorFromRGB(0x00C3F3) forState:UIControlStateNormal];
[_registerButton addTarget:self action:@selector(registerButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_backgroundView];
[self.view addSubview:_registerButton];
[_backgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.view);
}];
[_registerButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(140, 45));
make.bottom.mas_equalTo(self.view).offset(-25);
make.left.mas_equalTo(self.view).offset(32);
}];
}
在这个欢迎界面以及大多数界面中,由于视图层的代码非常简单,我们很多情况下并不会去写一个单独的 UIView 类,而是将全部的视图层代码丢到了 UIViewController 中,这种情况下甚至也没有 Model 层,Controller 承担了全部的工作。

上述的代码对视图进行了初始化,将需要展示的视图加到了自己持有的根视图中,然后对这些视图进行简单的布局。
当然我们也可以将视图的初始化单独放到一个类中,不过仍然需要处理 DRKBackgroundView 视图的布局等问题。
- (void)setupUI {
DRKBackgroundView *backgroundView = [[DRKBackgroundView alloc] init];
[backgroundView.registerButton addTarget:self action:@selector(registerButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:backgroundView];
[backgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.view);
}];
}
UIViewController 的这种中心化的设计虽然简单,不过也导致了很多代码没有办法真正解耦,视图层必须依赖于 UIViewController 才能展示。
惰性初始化
当然,很多人在 Controller 中也会使用惰性初始化的方式生成 Controller 中使用的视图,比如:
@interface ViewController ()
@property (nonatomic, strong) UIImageView *backgroundView;
@end
@implementation ViewController
- (UIImageView *)backgroundView {
if (!_backgroundView) {
_backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"backgroundView"]];
}
return _backgroundView;
}
@end
这样在 -viewDidLoad 方法中就可以直接处理视图的视图层级以及布局工作:
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.backgroundView];
[self.backgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.view);
}];
}
惰性初始化的方法与其他方法其实并没有什么绝对的优劣,两者的选择只是对于代码规范的一种选择,我们所需要做的,只是在同一个项目中将其中一种做法坚持到底。
处理用户行为
在 UIViewController 中处理用户的行为是经常需要做的事情,这部分代码不能放到视图层或者其他地方的原因是,用户的行为经常需要与 Controller 的上下文有联系,比如,界面的跳转需要依赖于 UINavigationController 对象:
- (void)registerButtonTapped:(UIButton *)button {
RegisterViewController *registerViewController = [[RegisterViewController alloc] init];
[self.navigationController pushViewController:registerViewController animated:YES];
}
而有的用户行为需要改变模型层的对象、持久存储数据库中的数据或者发出网络请求,主要因为我们要秉承着 MVC 的设计理念,避免 Model 层和 View 层的直接耦合。
存储当前界面的状态
在 iOS 中,我们经常需要处理表视图,而在现有的大部分表视图在加载内容时都会进行分页,使用下拉刷新和上拉加载的方式获取新的条目,而这就需要在 Controller 层保存当前显示的页数:
@interface TableViewController ()
@property (nonatomic, assign) NSUInteger currentPage;
@end
只有保存在了当前页数的状态,才能在下次请求网络数据时传入合适的页数,最后获得正确的资源,当然哪怕当前页数是可以计算出来的,比如通过当前的 Model 对象的数和每页个 Model 数,在这种情况下,我们也需要在当前 Controller 中 Model 数组的值。
@interface TableViewController ()
@property (nonatomic, strong) NSArray<Model *> *models;
@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 层对界面之间的跳转进行操作。
当然,我们也可以引入 Router 路由对 UIViewController 进行注册,在访问合适的 URL 时,通过根 UINavigationController 进行跳转,不过这不是本篇文章想要说明的内容。
UINavigationController 提供的 API 还是非常简单的,我们可以直接使用 -pushViewController:animated: 就可以进行跳转。
RegisterViewController *registerViewController = [[RegisterViewController alloc] init];
[self.navigationController pushViewController:registerViewController animated:YES];
作为数据源以及代理
很多 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 层的数据。
- (void)registerButtonTapped:(UIButton *)button {
LoginManager *manager = [LoginManager manager];
manager.countryCode = _registerPanelView.countryCode;
...
[manager startWithSuccessHandler:^(CCStudent *user) {
self.currentUser = user;
...
} failureHandler:^(NSError *error) {
...
}];
}
当按钮被点击时 LoginManager 就会执行 -startWithSuccessHandler:failureHandler: 方法发起请求,并在请求结束后执行回调,更新 Model 的数据。
小结
iOS 中 Controller 层的职责一直都逃不开与 View 层和 Model 层的交互,因为其作用就是视图层的用户行为进行处理并更新视图的内容,同时也会改变模型层中的数据、使用 HTTP 请求向服务端请求新的数据等作用,其功能就是处理整个应用中的业务逻辑和规则。
但是由于 iOS 中 Controller 的众多职责,单一的 UIViewController 类可能会有上千行的代码,使得非常难以管理和维护,我们也希望在 iOS 中引入新的架构模式来改变 Controller 过于臃肿这一现状。
几点建议
Controller 层作为 iOS 应用中重要的组成部分,在 MVC 以及类似的架构下,笔者对于 Controller 的设计其实没有太多立竿见影的想法。作为应用中处理绝大多数逻辑的 Controller 其实很难简化其中代码的数量;我们能够做的,也是只对其中的代码进行一定的规范以提高它的可维护性,在这里,笔者有几点对于 Controller 层如何设计的建议,供各位读者参考。
不要把 DataSource 提取出来
iOS 中的 UITableView 和 UICollectionView 等需要 dataSource 的视图对象十分常见,在一些文章中会提议将数据源的实现单独放到一个对象中。
```
void (^configureCell)(PhotoCell, Photo) = ^(PhotoCell* cell, Photo* photo) {
cell.label.text = photo.name;
};
photosArrayDataSource = [[ArrayDataSource alloc] initWithItems:photos