美文网首页
MVX中的Controller

MVX中的Controller

作者: _我和你一样 | 来源:发表于2017-12-08 00:13 被阅读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到底是什么,有何差异。

    相关文章

      网友评论

          本文标题:MVX中的Controller

          本文链接:https://www.haomeiwen.com/subject/djssixtx.html