MVX中的Controller

2017-12-08  本文已影响0人  _我和你一样

MVX是什么?

MVC、MVVM、MVP...

模型层和视图层职责非常清楚,一个用于处理本地数据的获取以及存储,另一个用于展示内容、接受用户的操作与事件。在这种情况下,应用中的其他功能和逻辑就会被自认而然的扔到X层中。

这个X在MVC中就是Controller层、在MVVM中就是ViewModel层、在MVP中就是Presenter层。这里介绍的时MVC中的控制器层Controller。

下面是iOS中对MVC的隔层交互的最简单的说明:

MVC

视图层和模型层分开,由Controller层协调。视图接受用户行为,Controller处理用户行为,更新模型,模型变更通知Controller,Controller更新视图。

总的来说,Controller层要负责以下问题:

  1. 管理根视图的声明周期和应用声明周期;
  2. 负责将视图层的View对象添加到根视图上;
  3. 负责处理用户行为,比如UIButton的点击及手势的触发;
  4. 储存当前界面状态;
  5. 处理界面之间的跳转
  6. 作为UITableView以及其他容器视图的代理
  7. 负责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 提供了 UINavigationControllerUITabBarController 这两种容器 Controller,所以 iOS 中界面跳转的这一职责大部分都落到了 Controller 上了。

iOS总共有三种界面跳转方式:

  1. UINavigationController 中使用push和pop改变栈顶的UIViewController对象;

  2. UITabBarController中点击各个UITabBarItem实现跳转;

  3. 使用所有的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 中的 UITableViewUICollectionView 等需要 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大体上由这些部分组成:

  1. 声明周期及一些需要重写的方法
  2. 视图层代码的初始化
  3. 各种数据源和代理协议的实现
  4. 事件、手势和通知的回调
  5. 实例变量的存取方法
  6. 一些其他的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到底是什么,有何差异。

上一篇下一篇

猜你喜欢

热点阅读