×

iOS的MVC框架之控制层的构建(下)

96
欧阳大哥2013 595a1b60 08f6 4beb 998f 2bf55e230555
2018.02.23 08:41* 字数 11490

在我的iOS的MVC框架之控制层的构建(上)一文中介绍了一些控制层的构建方法,而这篇文章则继续对一些方法进行展开讨论。MVC被众多开发者所诟病的C层的膨胀,究其原因不外乎有如下几点:

  1. 所有视图的构建和布局代码都在控制器中完成。有很多同学不喜欢系统提供的Storyboard和XIB来构建视图,而是喜欢通过代码的形式来完成视图界面布局,并且通常这部分代码都集中在loadView或者viewDidLoad或者通过懒加载的形式分散在各处。通过代码来构建和布局视图的代码量有可能会超过您视图控制器总代码量的50%。
  2. 对服务端的请求,往往就是包装了一层非常薄的请求层,通常称之为APIService。 这部分代码只是简单封装了对服务端URL的请求,同时通过一些报文转数据模型的第三方框架直接将报文转化为数据模型并通过异步回调的形式回吐给控制器或者视图。APIService的简单实现却增加了控制器的负荷,导致控制器除了要构建视图并且请求网络服务外还要担负非常多的一部分业务逻辑的实现。
  3. 对于一些复杂展示逻辑的功能界面没有进行合理拆解和有效设计导致所有代码都在一个视图控制器内完成,从而导致控制器膨胀臃肿。
  4. 在应用中最多使用的UITableView以及UITableViewCell中的数据更新的处理机制使用不恰当导致delegate中的方法实现异常的复杂,尤其是那些复杂的UITableViewCell的更新处理不得当导致代码混乱不堪。

可以看出框架本身没有问题,问题在于使用的人不了解或者不恰当的设计思想导致问题出现了。当出现问题时我们首先应该反思的是自己哪里不对而不是去怪别人哪里不对。(这个鸡汤撒得真LOW!!) 怎么解决上面所说的导致C层膨胀的几个问题呢?这也是这篇文章所要重点介绍的。

不同代码的构建时机

控制器类是一个功能的调度总控室,而且他还通过模板方法的设计模式提供给了我们在控制器的生命周期内各阶段事件发生时的处理回调。比如控制器构建时(init)、 视图构建时(loadView)、视图构建完成时(viewDidLoad)、视图将要呈现到窗口前(viewWillAppear)、视图已经呈现到窗口(viewDidAppear)、视图将要从窗口删除(viewWillDisappear)、视图已经从窗口删除(viewDidDisappear)、视图被销毁(viewDidUnload,这个方法在iOS6.0以后将不起作用了)、控制器被销毁(dealloc)。为了实现功能,我们可能需要在上述的某个地方添加对应的处理代码。如何添加代码?以及在上述的模板方法中添加什么样的代码?就非常的关键了。在这里面我想强调一点的是虽然控制器中拥有了一个view的根视图属性,但是控制器的生命周期一般要比根视图的生命周期要长,而且有可能会出现一个功能在不同场景下的视图呈现完全不一样,或者有可能会通过重新构建视图来实现一些换肤功能的场景。在iOS6以后的控制器中只提供了视图构建以及构建完成的模板方法,但却不再提供视图被销毁之前或者之后的模板方法,因此我们在loadView以及viewDidLoad中添加代码时就一定要考虑到这么一点,因为他不像其他的方法一样提供了互逆处理的机制。

  • 控制器初始化(init)
    如果你的业务模型对象的生命周期和控制器的生命周期一样,那么建议将业务模型对象的构建放在控制器的初始化代码中,当然前提是你的业务模型对象是一个轻量级的对象,如果你的业务模型对象的构建特别消耗时间那么不建议放在控制器的初始化中构建而是通过懒加载或者在某个触摸事件发生时再构建。如果你的控制器由多个子控制器组成,那么子控制器的初始化工作也在这里完成最佳。在控制器初始化时我们还可以初始化以及创建一些其他的轻量级的属性,这些属性或者变量的生命周期和控制器的生命周期一致。

  • 视图构建(loadView)
    如果你的视图是通过SB或者XIB来建立的,那么恭喜你,你可以省略这部分代码。如果你是通过代码来构建你的视图,那么你就有必要在这个地方添加你的视图构建和布局代码。你需要重载loadView的方法,并在最好在这里完成所有视图的构建和布局。如果你想复用默认的根视图作为自己的根视图那么你需要在构建你的其他子视图之前调用基类的loadView方法,而如果你想要完全构建自己的根视图以及子视图体系那么你就不必要调用基类的loadView方法。很多人都喜欢在viewDidLoad里面进行视图的构建,其实不是最佳的解决方案,因为根据字面意思viewDidLoad里面添加的应该是视图构建并加载完成后的一些处理逻辑。如何在loadView中更加优雅以及合理的构造界面布局代码,后面我将会给出一个具体解决方案。

-(void)loadView
{
   /*
   自定义根视图的构建,不需要调用基类的方法。你也可以直接在这里将UIScrollView或者UITableView作为根视图。
   这样就不必在默认的根视图上再建立滚动视图或者列表子视图了。
   */
    self.view = [[UIView alloc] initWithFrame: [UIScreen mainScreen].bounds];

   //...建立其他子视图。

}

  • 事件绑定的代码(viewDidLoad)
    当视图构建完毕后系统会调用viewDidLoad。因此您应该在这里完成一些业务逻辑初始化的动作、业务模型服务接口的初始请求、一些控件的事件处理绑定的动作、视图的delegate以及dataSource的设置。也就是这里一般用来完成视图和控制器之间的关联处理以及控制器和业务模型的关联处理。在viewDidLoad中最适合做的就是实现视图和控制器之间的绑定以及控制器和业务模型之间的绑定操作。这里不建议进行视图的构建,以及一些涉及到整个控制器生命周期相关的处理。

  • 视图的呈现和消失(viewWill/DidAppear,viewWill/DidDisappear)
    视图的呈现和消失有可能会被反复调用。建议在这里完成定时器、通知观察者的添加和销毁处理。一般来说定时器和观察者都只是在界面被呈现时产生作用,而界面消失时则不处理,因此在这里添加定时器和通知观察者是最合适的。而且还有一个好处就是在这里实现定时器和观察者时不会产生循环引用而导致控制器不能被释放的问题发生。

  • 控制器被销毁(dealloc)
    控制器被销毁时表明控制器的生命周期已经完结了。一般情况下不需要添加特殊的代码,这里一再强调的就是:
    一定要在这里把各种控件视图中的delegate以及dataSource设置为nil!
    一定要在这里把各种控件视图中的delegate以及dataSource设置为nil!
    一定要在这里把各种控件视图中的delegate以及dataSource设置为nil!

重要的事情说三遍!不管这些delegate是assign还是weak的。

懒加载

懒加载的目的是为了解决按需创建使用以及可选使用以及耗时创建的场景。在某种情况下使用懒加载可以加快展示的速度,懒加载可以将某些对象的创建时机延后。那么是不是要将所有的对象的创建都采用懒加载的形式进行创建? 答案是否定的。 有不少同学都喜欢将控制器中的所有视图的创建和布局都通过懒加载的形式来完成,如下面的代码片段:

@interface XXXViewController()
   @property(strong) UILabel *label;
   @property(strong) UITableView *tableView;
@end

@implementation XXXViewController

-(UILabel*)label
{
     if (_label == nil)
    {
          _label = [UILabel new];
          [self.view addSubview:_label];
         //有些同学会在这里添加附加代码比如布局相关的代码
    }
    return _label;
}

-(UITableView*)tableView
{
     if (_tableView == nil)
    {
           _tableView = [UITableView new];
          [self.view addSubview:_tableView];
          _tableView.delegate = self;
         //有些同学会在这里添加附加代码比如布局相关的代码
    }
    return _label;
}


-(void)viewDidLoad
{
    [super viewDidLoad];

    self.label.text = @"hello";
    [self.tableView reloadData];
}


@end

看起来代码很简洁也很清晰,起码在viewDidLoad中是这样的。但是这里面却有可能存在着一些隐患:

  • 视图层次顺序被打乱和代码分散
    因为视图都是懒加载并且分散的,因此你不能从整体看出视图层次结构是如何的,以及排列的顺序是如何的。这就为我们的代码阅读以及调试和维护增加了困难。

  • 职责不明确
    懒加载的主要作用是延迟创建,但是上述的视图属性的重写却已经超出了单纯的创建的范畴了,除了创建视图之外还实现了视图添加到父视图的功能以及进行布局的功能,更有甚者还有可能实现其他更加复杂的逻辑。这样就会导致一个get属性的实现承载的功能过多,严重的超过了一个方法所应承担的责任。在使用时我们只是简单的将其当做一个读取属性来使用并且还有可能发生有些代码重复的问题。

  • 莫名的问题和崩溃
    懒加载视图使得我们的视图属性必须要设置为strong类型的,而且代码的实现是只创建一次。如果因为某些原因使得我们的控制器里面的所有视图都需要重新创建(比如换肤)时那么就有可能导致这个懒加载的视图不会再次被创建而产生界面上莫名其妙的问题。更有甚者因为在懒加载中实现过多的代码导致在某些地方访问属性时产生了崩溃。

因此不建议对一个控制器里面的所有视图构建都采用懒加载模式,视图的构建和布局应该在loadView中进行统一处理。懒加载的方式不能滥用,尤其是视图的构建代码。我们应该只对那些可选存在的对象以及那些有可能会影响性能的对象采用懒加载的方式来进行构建,而不是所有的对象都采用懒加载的形式来创建。同时还需要注意的就是如果一定要采用懒加载来实现对象的构建时,在懒加载中的代码也应该尽量的简化,只需要实现创建部分的功能即可,而不要将一些非必要的逻辑代码放入到懒加载的实现处,越多的逻辑实现,就会对使用着产生越多的限制和不确定因素的发生。就以上面的例子来说使用者在调用self.label或者self.tableView时一般都只是将它们当做普通的属性来使用,而不会去考虑它们的内部还进行了如此多的设置和处理(比如完成布局和添加到父视图中去)。这样就可能会造成对这些属性的使用不当而造成灾难的后果。另外虽然你的视图的构建是通过懒加载的形式来完成的,但是如果你在比如viewDidLoad中大量的访问这些属性时一样的会产生视图的构建操作,这样其实和直接创建视图对象是一样的,并没有起到任何优化性能的作用,而且这样也是和懒加载的初衷是违背的。

我们项目中的一个案例就是UITableView的创建使用的懒加载,里面除了创建UITableView的实例外还在里面设置了delegate的值以及其他代码逻辑。而这个UITableView又刚好是一个可选的显示视图。同时我们又在视图控制器的dealloc中对这个UITableView的delegate做了置为nil的处理。结果这段代码最终在线上出现了crash的情况了。

简化控制器中的视图构建

视图的构建有两种方式:一种是通过Storyboard或者XIB以可视化的方式来构建;一种是通过程序代码的方式来完成构建。两种方法各有优劣。iOS以及Android系统都提供了强大的可视化界面布局系统,并且二者都是采用XML文件的方式来描述布局。这种方式非常符合MVC中关于V的定义,视图部分独立存在并且层次分明。采用这种方式来构建你的视图在一定程度上不会对你的控制器中的代码产生污染以及导致你控制器中的代码的膨胀。通过SB和XIB的使用就可以简化我们对视图部分的构建。在实践中你会发现如果你是通过代码来完成视图的构建和布局那么这部分代码就有可能超过你控制器50%的代码行数。因此解决C层臃肿的一个方法就是将你的界面布局的代码都统一通过SB或者XIB来实现。有的同学可能会说通过SB或者XIB的方式不利于协同开发,很容易造成合并时的代码冲突。其实这是一个伪命题。一般情况下我们的功能都会拆分为一个个视图控制器来实现,并且一个人负责一个控制器。如果你用XIB来实现自己负责的那个控制器的界面布局那么又怎么可能会产生代码合并的冲突呢?即使是你用SB的方式来构建你的界面,虽然SB是将大部分界面都放在一个文件中来完成,但是在实践中我们的应用是可以建立多个SB的。我们可以从功能相似性的角度出发将相同的功能放在一个SB中,不同大模块建立不同的SB文件,这样就可以将一个SB根据应用模块分解为多个小SB。只要拆分的合理那么在进行协同开发时就会最大限度的减少冲突的发生。随着XCODE版本的更新,SB所具有的功能越来越强大,通过SB除了能实现界面布局外包括逻辑的跳转以及页面的切换我们都不需要编写一行代码。我们其实可以花一点时间静下心来好好的去研究一下它,而不是一味的去拒绝和抵触。君不见Android的开发者还是喜欢通过XML并且基本是通过XML的编写来完成界面布局的呢。

也许上面的方式说不服你,你还是通过代码来构建布局那一派的。没有关系,本文探讨的是如何解决控制器代码膨胀的问题,而不是掀起派系之争。那么如果我就是要通过代码的方式来完成界面布局呢?毕竟通过代码布局的方式更加灵活和可配置性(牺牲了所见即所得性)。我们知道在iOS的loadView的默认实现逻辑是首先会到SB或者XIB中去根据视图控制器的类型去搜索是否有匹配的视图布局文件,如果有则将这个视图布局文件进行解析并构建对应的视图层次树并设置视图控制器中的那些插座变量(IBOutlet)以及绑定视图控件所关联的事件处理器(IBAction)。如果没有找到对应的布局文件的话就会创建一个空白的根视图(self.view)。可见loadView的主要目的就是为了完成视图的构建和布局。因此当我们通过代码的方式来完成视图的创建以及布局时也应该将代码逻辑放到这里而不应该放到viewDidLoad中去。视图的构建和布局应该在一个地方统一进行而不应该通过懒加载的方式来将代码分散到对各个视图属性进行重写来完成。 在这里我提供2种方法来实现视图构建和布局从控制器中分离或者归类处理。

一. 采用分类扩展的方法

顾名思义,采用分类扩展的方法就是为视图控制器专门建立一个视图构建和布局的分类扩展。为了将这部分代码和控制器中其他代码分离,我们可以将视图构建的分类扩展代码单独放到新文件中来实现。

//为每个控制器都建立一个 控制器名字+CreateView的头文件
//XXXXViewController+CreateView.h
#import "XXXXViewController.h"

//定义一个扩展,扩展里面定义所有控制器可能要用到的视图属性,定义属性的方式就和通过SB或者XIB的方式一致。
@interface XXXXViewController ()
  @property(nonatomic, weak) IBOutlet UILabel *label;
  @property(nonatomic, weak) IBOutlet UIButton *button;
  @property(nonatomic, weak) IBOutlet UITableView *tableView;
   //...
@end

..................................
//代码布局的实现部分
//XXXXViewController+CreateView.m

#import "XXXXViewController+CreateView.h"

//这里定义一个分类,分类只实现loadView的重载来完成视图的构建和布局
@implementation XXXXViewController(CreateView)

-(void)loadView
{
    [super loadView];   //如果你想完全自定义根视图就可以和上面我曾经列出的代码一样不调用父类的方法。

  //这里完成所有子视图的构建和布局。因为视图构建的代码都是统一写在一起的,所以这里面就可以很方便的通过阅读代码的方式来看清怎么视图的布局层次。

    UILabel *label = [UILabel new];
    label.textColor = [UIColor redColor];
    label.font = ....
    [self.view addSubview:label];
    self.label = label;

    UIButton *button = [UIButton new];
    [self.view addSubview:button];
    self.button = button;

    UITableView *tableView = [UITableView new];
    [self.view addSubview:tableView];
    self.tableView = tableView;

   //....

   //你可以在这里对上面所有的子视图通过autolayout的方式来完成代码布局的编写、也可以在上面每个视图创建完成后就进行代码布局的编写,这个没有限制。

}

@end

上面的代码可以看出我们单独建立了一个扩展来定义所有视图属性,并建立了一个分类并且重载loadView来实现视图的建立和布局。代码中我们只做构建和布局,而不做其他的事情。比如UIButton的事件绑定以及UITableView的delegate和dataSource的设置都不在这里面进行。这个分类就是一个非常存粹的代码构建和界面布局的代码。这样我们看下面的控制器的主要代码实现部分就非常的干净了。

//XXXXViewController.h

@interface  XXXXViewController

@end

..............................
//XXXXViewController.m

//这里导入分类为了能够访问其中的视图属性
#import XXXXViewController+CreateView.h

@implementation  XXXXViewController

-(void)viewDidLoad
{
    [super viewDidLoad];
    
     //这里对按钮绑定事件,对tableView指定委托和数据源,可以看出在viewDidLoad里面最适合做的事情就是建立视图和控制器之间的关联和绑定。
     [self.button  addTarget:self action:@selector(handleClick:) forControlEvents:UIControlEventTouchUpInside];
     self.tableView.delegate = self;
     self.tableView.dataSource = self;
}

@end

通过分类扩展的方法并不能减少控制器的代码,但是却可以将特定的逻辑进行归类分解,从而增强代码的可阅读性以及可维护性。因为关于视图构建和布局部分的代码都拆分到其他单独的地方,而我们的控制器的主要实现部分就可以专心编写控制逻辑了。甚至这种拆分的方法还可以将工作一分为二:一人专门负责界面布局、一人专门负责控制逻辑的编写。

二. 采用接口和消息转发

视图控制器通过对分类扩展来实现视图构建的拆分,代码还是属于视图控制器的一部分。如果我们想完全实践MVC中的V独立存在并且可以被复用的话,我们可以将视图构建和布局单独抽象到一个视图类中,并且通过接口定义和消息转发的方法来建立控制器和视图之间的联系。还记得我在上一篇文章里面所提到的forwarding技术吗?为了实现视图和控制器的分离我们依然可以采用这种方法来实现层次的分离。

  • 1.定义视图属性接口和视图布局类
//定义一个以控制器名开头加View的协议和实现类。
//XXXXViewControllerView.h

@protocol  XXXXViewControllerView

@optional
  @property(nonatomic, weak)  UILabel *label;
  @property(nonatomic, weak)  UIButton *button;
  @property(nonatomic, weak)  UITableView *tableView;
  //...
@end


//你的布局根视图可以继承自UIView或者UIScrollView或者其他视图。
@interface XXXXViewControllerView:UIView<XXXXViewControllerView>
  @property(nonatomic, weak) IBOutlet UILabel *label;
  @property(nonatomic, weak) IBOutlet UIButton *button;
  @property(nonatomic, weak) IBOutlet UITableView *tableView;
@end

................................
//XXXXViewControllerView.m

@implementation  XXXXViewControllerView

-(id)initWithFrame:(CGRect)frame
{
   self = [super initWithFrame:frame];
   if (self != nil)
   {
           self.backgroundColor = [UIColor whiteColor];

           UILabel *label = [UILabel new]; 
           [self addSubview:label];
           _label = label;

            UIButton *button = [UIButton new];
            [self addSubview:button];
            _button = button;

           UITableView *tableView = [UITableView new];
           [self addSubview:tableView];
           _tableView = tableView;

           //如果您用的是AutoLayout那么您可以在这里添加布局约束的代码。如果您是通过frame来进行布局那么请在layoutSubviews中进行子视图的布局处理。
   }

    return self;
}

-(void)layoutSubviews
{
    [super layoutSubviews];
    
    //如果你是通过frame来设置布局那么就可以在这里进行布局的刷新。。
}


@end

可以看出上述的代码和控制器之间没有任何关系,并且是独立于控制器而存在的。视图布局类的作用就是只用于视图的布局和构建以及展示,这种方式非常符合MVC中V的定义和实现。视图构建完成后,需要对视图进行布局处理,您可以使用AutoLayout方式来进行布局也可以使用frame方式来进行布局。AutoLayout布局是一种通过视图之间的约束设置来实现布局的方式,而frame方式则是苹果早期的一种布局方式。AutoLayout进行代码布局时,代码量非常的多和复杂,这个问题在iOS9以后简化了很多。还好有很多第三方的布局类库比如Mansory可以有效的简化布局的难度。如果您的布局要考虑性能问题以及想更加简单的完成布局那么您可以考虑使用笔者开源的界面布局:MyLayout来实现界面布局。

  • 2.视图控制器和布局视图类的绑定。
//XXXXViewController.h


@interface XXXXViewController:UIViewController
@end

................................
//XXXXViewController.m

#import "XXXXViewControllerView.h"  //这里导入对应的布局视图类

//视图控制器也需要实现XXXXViewControllerView接口。这样视图控制器中就可以直接访问视图的一些属性了。
@interface XXXXViewController ()<XXXXViewControllerView>

@end

@implementation XXXXViewController

//重写loadView来完成视视图的构建。
-(void)loadView
{
    self.view = [[ViewControllerView alloc] initWithFrame:[UIScreen mainScreen].bounds];
}

//这个部分是实现的关键,来将控制器对视图属性协议的访问分发到布局视图中去。
-(id)forwardingTargetForSelector:(SEL)aSelector
{
    struct objc_method_description  omd = protocol_getMethodDescription(@protocol(ViewControllerView), aSelector, NO, YES);
    if (omd.name != NULL)
    {
        return self.view;
    }

    return [super forwardingTargetForSelector:aSelector];

}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    //这里就可以像平常一样访问视图属性并添加事件的绑定处理。
    [self.button addTarget:self  action:@selector(handleClick:) forControlEvents:UIControlEventTouchUpInside];
    
}

-(void)handleClick:(id)sender
{
    
}

@end

大家可以看到上面通过对loadView和forwardingTargetForSelector方法进行重载来实现视图控制器与视图之间的绑定。然后我们就可以在任意位置来访问视图接口中的属性了。绑定操作对于所有视图控制器类来说都是一致的,所以你可以通过一个宏定义的形式来实现上面的绑定操作:

//在某个公共的地方定义如下宏

#define BINDVIEW(viewclass)   \
-(void)loadView \
{\
    self.view = [[viewclass alloc] initWithFrame:[UIScreen mainScreen].bounds];\
}\
-(id)forwardingTargetForSelector:(SEL)aSelector\
{\
    struct objc_method_description  omd = protocol_getMethodDescription(@protocol(viewclass), aSelector, NO, YES);\
    if (omd.name != NULL)\
    {\
        return self.view;\
    }\
    return [super forwardingTargetForSelector:aSelector];\
}\

...........................

//XXXXViewController.m

#import "XXXXViewControllerView.h"

//视图控制器也需要实现XXXXViewControllerView接口。这样视图控制器中就可以直接访问视图的一些属性了。
@interface XXXXViewController ()<XXXXViewControllerView>

@end

@implementation XXXXViewController

//这里直接用宏即可
BINDVIEW(XXXXViewControllerView)

//...这里添加其他代码。

@end

上面的两种对视图构建和布局进行分解的方式都可以解决在控制器中视图代码构建导致的膨胀问题。第一种方法本质上只是做了一些代码拆分,并未实现控制器和视图的完全分离;第二种方法则完全实现了视图和控制器之间的分离,视图的构建和布局不再依赖于控制器的存在,而且我们甚至可以对视图进行复用,也就是说可以让多个控制器类复用一个视图类中的代码。这些控制器所实现的功能的展示效果一样或者有微小的差别,但是事件处理逻辑则可以完全不一样。第二种方法的实现机制更加体现了MVC中的层次关系以及V层构建的独立性。因此不管你是通过SB或者XIB来构建您的视图还是通过代码来构建您的视图布局,只要设计得当都可以非常有效的减少视图控制器中对视图依赖部分的代码。

业务逻辑的下沉

视图的构建部分的问题我们已经成功解决。我们再来探讨一下薄服务层APIService的问题。在开始我曾经说过很多的架构设计人员都会以和服务器之间交互的所有API接口为标准而设计出一套服务层API,我们姑且叫他为APIService。APIService会为每一个和服务端交互的接口都产生一个简单的封装,这个封装只是完成了对向服务器请求的数据的打包以及URL链接的封装以及将服务端返回的报文进行反序列化解包后直接通过block回调的方式返回给视图控制器。


@interface APIService:NSObject

  +(void)requestXXXWithDict:(NSDictionary*)input  callback:(void (^)(XXXXModel *model, NSError *error))callback;

  +(void)requestYYYYWithDict:(NSDictionary*)input  callback:(void (^)(YYYYModel *model, NSError *error))callback;

   //.....
@end

我们的视图控制器中的任何一个网络请求都是直接调用对应的请求方法,并对返回的Model数据模型进行加工处理,比如界面视图数据刷新、文件处理、某些逻辑的调整等等。在这个过程中控制器就无形之中承担了业务逻辑的实现的工作,从而加重了控制器中代码的负担。比如下面的代码例子:

@ implementation XXXXViewController

//某个控制器的某个事件处理代码。
-(void)handleClick:(id)sender
{
     //这部分代码需要根据不同的状态来请求不同的服务。假设这个状态值保存到控制器中

    if (self.status == 1)
    {
          //弹出loading... 等待框 ,并请求服务
          [APIService  requestXXX:^(XXXModel* model, NSError *error){
                   //销毁loading... 框
                  if (error == nil)
                  {

                       //将model写入某个文件中去。
                       // 将model的数据更新到某个视图中去。
                       self.status = 2;   //更新状态。
                       //其他逻辑。。
                 }
                else
                {
                   //..错误处理。
                }   
         }];
   }
   else if (status == 2)
   {
        //弹出loading... 等待框,并请求另外一个服务,返回的数据模型相同。
       [APIService requestYYY:^(XXXModel *model, NSError *error){
                   //销毁loading... 框
                  if (error == nil)
                  {

                       //将model写到文件中或者更新到数据库中去。
                      // 将model的数据更新到某个视图中去。
                      self.status = 1;   //更新状态。
                      //其他逻辑。。
                 }
                else
                {
                   //..错误处理。
                }   
         }];
   }   
}

@end

上面的代码可以看出控制器除了保存一些状态外,并且根据不同的状态还做了不同的网络服务请求、文件的读写、状态的更新、视图的刷新操作等等其他逻辑,这样就导致了控制器的代码非常的臃肿和难以维护。问题出在哪里了呢?就是对模型层的理解产生了误区,以及对服务层的定义产生了错误的使用。

真实的MVC中的M模型层所代表的是业务模型而非数据模型、业务模型的作用就是用来完成业务逻辑的具体实现。M层所要做的就是将一些和视图展现无关以及和控制器无关的东西进行封装处理,而只是给控制器提供出非常简单易用的接口来供其调用。APIService的封装是不符合逻辑和错误的封装的!我们知道任何系统都有一套完整的业务实现体系,这个实现体系不止在服务器端存在而且在客户端上也存在,这两者之间是一致的。您可以将业务实现的体系理解为服务端实现体系的一个代理,代理和服务器服务之间通信的纽带就是接口报文。
我们不能将客户端的代理实现简单理解为只是对接口报文的简单封装,而是应该设计为和服务端一样具有完整架构体系的业务逻辑实现层,这我想也就是M层的本质所在吧。所以我们在设计客户端的M层时也一定要本着这个思想去设计,不能只是简单的为接口报文进行封装,并且在控制器里面去实现一些业务逻辑,而是应该将业务逻辑的实现、网络的请求、报文的处理以一种抽象的以及和业务场景相关的东西统一的放在M模型层。这种理念和设计方法其实在我的另外两篇介绍模型层构建的文章中都非常详细的有说明。我们应该在某种程度上将原先属于在控制器中的逻辑进行下沉和分解来将逻辑的实现部分下移到模型层,这样我们在设计时就不会只是简单的实现一个一个APIService中的方法。而是构建出一套完整的业务模型框架出来供控制器来使用了。还是以上面的例子,解决的方法是我们设计出一个业务模型类比如XXXXService,它内部封装了状态以及不用的网络请求,以及一些文件读写的实现:

//XXXXService.h

 @interface XXXXService
    
     -(void)request:(void (^)(XXXModel *model, NSError *error))callback;

  @end  

..........................
//XXXXService.m

@ implementation XXXXService
{
    int status = 1;
}

-(void)request:(void (^)(XXXModel *model, NSError *error))callback
{
       if (self.status == 1)
       {
             [network   get:@"URL1"   complete:^(id obj, NSError *error){
                 XXXModel *retModel = nil;
                 if (error == nil)
                  {
                       XXXModel *retModel = obj --> XXXModel //报文到模型的第三方转换工具
                       //这里写入文件和数据库
                       self.status = 2;  //这里更新状态。
                   }
                   callback(retModel, error);
             }];
      } 
     else if (self.status == 2)
     {
         [network   get:@"URL2"   complete:^(id obj, NSError *error){
              XXXModel *retModel = nil;
              if (error == nil)
              {
                    XXXModel *retModel = obj --> XXXModel //报文到模型的第三方转换工具,假设URL2和URL1的数据模型都非常相似
                   //这里做其他的非视图相关的逻辑。
                   self.status = 1;  //这里更新状态。
             }
             callback(retModel, error);
         }];
     }
}

@end

上面的业务模型代码只是纯粹的逻辑实现和具体的控制器无关和具体的视图无关。那么我们如何在控制器中使用这个业务模型呢?

//XXXXViewController.m

 #import "XXXXService.h"
      
@interface XXXXViewController()
   @property(strong)   XXXXService *service;  //将业务模型以对象的形式保存起来,这里我们将看不到单例对象、也看不到平面的服务请求了,而是一个普通的对象。而且是一个真实的对象!!!
@end
   
 @implementation  XXXXViewController
          
//至于service的创建方式可以在控制器初始化时创建,也可以通过懒加载的方式进行创建。这里我们通过懒加载的形式进行创建。这里才是懒加载的最佳实践      
 -(XXXService*)service
{
    if (_service == nil){
       _service = [XXXService new];
      } 
   return _service;
}            

//还是原来的事件处理函数
-(void)handleClick:(id)sender
{
       //弹出loading... 等待框 ,并请求服务
       [self.service  request^(XXXModel* model, NSError *error){
             //销毁loading... 框
             if (error == nil){   
                  // 将model的数据更新到某个视图中去。
             }
            else
           {
              //..错误处理。
            }       
         }];
}
  
@end

可以看出上面我们的视图控制器中的代码已经非常的简洁了,控制器不再持有状态,不再做一些业务实现相关的处理了,只是简单的调用业务模型提供的服务,并在回调中将数据模型中的数据更新视图就可以了。控制器不再根据状态去发起不同的请求,不再处理任务业务实现相关的东西,而且业务模型也不再是向以前那样干巴巴的使用单例或者使用类方法的形式提供给控制器调用,而是一个对象!一个真实的对象!一个面向对象中定义的对象来给控制器调用。通过对业务模型层的封装使得我们可以在其他的视图控制器中也非常简单的使用业务模型提供的服务来完成服务。从而精简了控制器中的代码和逻辑。在上面的例子中就可以很明确的看出MVC中M的责任负责业务逻辑的实现,V的责任就是负责视图的布局和展示,而C层的责任就是负责将二者关联起来。

控制逻辑的拆分

通过对视图类的封装和解耦解决了视图部分占用控制器的代码问题,通过对M层的正确定义解决了控制器过多的处理业务逻辑实现的问题。我们的控制器中的代码将会得到很大一部分的改善和精简。我们已经解决完了80%的问题了。可是即使如此我们的控制器中的逻辑有可能还是很多。

我们在构建的某个视图控制器中出现代码膨胀的一个非常重要的原因有可能是这个功能的逻辑非常的复杂或者界面展示非常的复杂:

  • 一个界面中同时集成了众多小的功能点,有些界面或者小功能点需要在特殊条件下才能展示出现。有些小功能界面是可选出现的。
  • 一个界面中分成了好几个区块来展示,每个区块之间相对独立,但又因为某些原因要集成在同一个页面之中。
  • 一个界面中受到某种状态的控制,在不同状态下可能会展示出完全不同的界面和实现完全不同的功能。

对于这些具有复杂逻辑的功能来说,如果设计的不得当就有可能出现控制器中的逻辑非常复杂和庞大。怎么解决这些问题? 答案还是分解。至于如何进行分解这就要具体问题具体分析了,这个就非常考验架构设计人员的技术和业务功底了。我们在这里不探讨如何进行业务拆分,而是讨论控制器对业务拆分的支持能力。
当某个控制器中的逻辑过于庞大和复杂时可以考虑将功能拆分为多个子控制器来实现

在iOS5以后系统提供了对子控制器的支持能力,子控制器和父控制器一样具有相似的生命周期内的各种方法的回调处理机制。子控制器的引入除了能够将视图布局进行拆分而且能够对处理逻辑进行拆分。在这种情况下我们把父视图控制器称为容器控制器。容器控制器的作用更多的是对整体进行调度和控制,它可能不会再具体负责业务,具体的业务由子控制器来完成。就如上面列出的三种场景我们都可以通过功能拆分的形式将一些逻辑拆分到子控制器来实现。我将分别用代码来举例上面的第二种和第三种场景的实现:

  • 这个是一个复杂界面由多个区域组成的实现场景
//ContainerVC.m

#import "SubVC1.h"
#import "SubVC2.h"
#import "SubVC3.h"


@interface  ContainerVC()
//这个功能被分为3个独立的区域进行展示和处理。
@property(nonatomic, strong)  SubVC1 *vc1;
@property(nonatomic, strong)  SubVC2 *vc2;
@property(nonatomic, strong)  SubVC3 *vc3;
@end

@implementation ContainerVC

- (void)viewDidLoad {
    [super viewDidLoad];
     
    //子视图控制器的构建,您可以在容器视图控制器的初始化方法init中处理也可以在viewDidLoad里面进行处理。
    //这里面先删除是为了防止有可能整个界面界面视图被重新初始化的情况发生
    [self.vc1 removeFromParentViewController];
    [self.vc2 removeFromParentViewController];
    [self.vc3 removeFromParentViewController];

    self.vc1 = [[SubVC1 alloc] init];
    self.vc2 = [[SubVC2 alloc] init];
    self.vc3 = [[SubVC3 alloc] init];

   //添加子视图控制器
    [self addChildViewController:self.vc1];
    [self addChildViewController:self.vc2];
    [self addChildViewController:self.vc3];

   //将子视图控制器里面的视图添加到容器视图控制器中的不同位置,当然您也可以用autolayout来进行布局
    [self.view addSubview:self.vc1.view];
    self.vc1.view.frame = CGRectMake(x, x, x, x);
    
    [self.view addSubview:self.vc2.view];
    self.vc2.view.frame = CGRectMake(x, x, x, x);

    [self.view addSubview:self.vc3.view];
    self.vc3.view.frame = CGRectMake(x, x, x, x);
  
}

@end

  • 这个是一个复杂界面由不同的状态变化驱动的场景

//ContainerVC.m

#import "SubVC1.h"
#import "SubVC2.h"
#import "SubVC3.h"


@interface  ContainerVC()
//这个功能根据不同的状态进行不同的处理

//状态
@property(nonatomic, assign) int status;
@property(nonatomic, strong) UIViewController *currentVC;   //当前的视图控制器

@end

@implementation ContainerVC

- (void)viewDidLoad {
    [super viewDidLoad];
    self.status = 1;   //设置当前状态。
}

-(void)setStatus:(int)status
{
    if (_status == status)
        return;
    
    _status = status
    [self.currentVC.view removeFromSuperview];
    [self.currentVC removeFromParentViewController];
    
    self.currentVC = nil;
    Class cls = nil;
    switch (_status) {
        case 1:
            cls = [SubVC1 class];
            break;
        case 2:
           cls =  [SubVC2 class];
            break;
        case 3:
           cls =  [SubVC3 class];
            break;
        default:
           NSAssert(0, @"oops!");
            break;
    }

    self.currentVC = [[cls alloc] init];  //这里可以带上容器视图里面的状态或者其他业务模型的参数来进行初始化
    [self addChildViewController:self.currentVC];
    [self.view addSubview:self.currentVC.view];
    self.currentVC.view.frame = self.view.bounds;
    
}

上面的两个场景都用到了子视图控制器的相关API。我们再来看看iOS中的关于子视图控制器的所有相关的API接口:

@interface UIViewController (UIContainerViewControllerProtectedMethods)

//得到一个父视图控制器里面的所有子视图控制器
@property(nonatomic,readonly) NSArray<__kindof UIViewController *> *childViewControllers;

//添加子视图控制器
- (void)addChildViewController:(UIViewController *)childController;

//将自己从父视图控制器中删除
- (void)removeFromParentViewController;

//如果我们要添加一个子视图控制器和删除一个子视图控制器同时执行并且要有动画效果时可以采用这个方法
- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))animations completion:(void (^ __nullable)(BOOL finished))completion;

//如果容器控制器想控制子视图控制器的呈现调用回调那么要重载容器控制器的shouldAutomaticallyForwardAppearanceMethods方法并返回NO。
//然后在适当的时候调用子视图控制器的下面这两个方法来实现呈现的自定义控制处理。
//这两个方法是对子视图控制器进行的调用,并且要成对执行。
- (void)beginAppearanceTransition:(BOOL)isAppearing animated:(BOOL)animated;
- (void)endAppearanceTransition;

// Override to return a child view controller or nil. If non-nil, that view controller's status bar appearance attributes will be used. If nil, self is used. Whenever the return values from these methods change, -setNeedsUpdatedStatusBarAttributes should be called.
@property(nonatomic, readonly, nullable) UIViewController *childViewControllerForStatusBarStyle;
@property(nonatomic, readonly, nullable) UIViewController *childViewControllerForStatusBarHidden;

// Call to modify the trait collection for child view controllers.
- (void)setOverrideTraitCollection:(nullable UITraitCollection *)collection forChildViewController:(UIViewController *)childViewController;
- (nullable UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController;

// Override to return a child view controller or nil. If non-nil, that view controller's preferred user interface style will be used. If nil, self is used. Whenever the preferredUserInterfaceStyle for a view controller has changed setNeedsUserInterfaceAppearanceUpdate should be called.
@property (nonatomic, readonly, nullable) UIViewController *childViewControllerForUserInterfaceStyle;

@end

@interface UIViewController (UIContainerViewControllerCallbacks)

//容器控制器可以重载这个方法来控制子视图控制器中的视图在添加到窗口以及从窗口删除时子视图控制器是否会自动调用viewWillAppear/viewDidAppear/viewWillDisappear/viewDidDisappear这几个方法,默认是YES。
//如果容器控制器重载这个方法返回NO时那么容器控制器就可以手动的让子视图控制器执行对应的呈现回调方法。
@property(nonatomic, readonly) BOOL shouldAutomaticallyForwardAppearanceMethods 

//子视图控制器将要移动到父视图控制器和已经移动到父视图控制器中时调用,子视图控制器可以重载这两个方法
- (void)willMoveToParentViewController:(nullable UIViewController *)parent;
- (void)didMoveToParentViewController:(nullable UIViewController *)parent;

@end



控制器的派生

对控制逻辑的拆分所用到的设计模式是所谓的组合设计模式,其本质是将功能分散到各个子模块中然后组合起来实现一个完整的大功能。并不是所有的场景都适合通过拆分以及组合的方式来解决问题。我们考虑一下下面的两个业务场景:

  • 两个功能界面相似但是处理逻辑不同或者界面不同但是处理逻辑相似
    一般的情况下因为是两个不同的功能也就是会用两个不同的控制器来实现,尤其是当这个两个功能属于不同的模块时更会如此。虽然两个功能之间有很多相似的东西,我们仍然有可能通过代码复制拷贝的方式来进行简单处理。但这并不是最佳的解决方案,因为通过代码复制的话就有可能会出现更新不一致的情况。我们也可以通过组合的形式来解决这个问题,但是组合的使用会在一定程度上增加代码量以及共享参数之间的传递问题,因此最佳的解决方案就是采用类继承的方法。就如当功能中界面相同的两个视图控制器只是处理逻辑不相同,那么我们只需要派生出一个新的类并覆盖掉基类的处理逻辑方法即可。
//VC1.h

@interface VC1:UIViewController
@end

......................
//VC1.m

@interface VC1()

@property(nonatomic, weak) UIButton *button;

@end

@implementation VC1

-(void)viewDidLoad
{
   [super viewDidLoad];

   [self.button addTarget:self  action:@selector(handleClick:) forControlEvents:UIControlEventTouchUpInside];
   //内部调用某个方法
   [self fn1];
}

-(void)handleClick:(id)sender
{
    //... VC1的事件处理逻辑。
}

-(void)fn1
{
    //VC1的逻辑。
}

@end


基类里面的handleClick方法以及fn1方法都是专门用来处理VC1的逻辑和事件的,现在我们要构造一个VC1的派生类VC2,派生类中界面相同但是事件处理逻辑以及一些方法则完全不同。我们可以覆写基类的对应的方法来实现逻辑的改变。

//VC2.h

//VC2从VC1处派生
@interface VC2:VC1
@end

.......................................
//VC2.m

//这里的声明一些派生类可以访问基类的一些属性和方法
@interface VC1()
@property(nonatomic, weak) UIButton *button;
@end


@implementation VC2

-(void)handleClick:(id)sender
{
    //... VC2的事件处理逻辑。
}

-(void)fn1
{
    //VC2的逻辑。因为基类的self.button在这里有声明,所以派生类是可以访问self.button属性的。
}

@end

通过上述的方法我们不用再通过代码复制来构建两个不同的视图控制器了,不同的场景启用不同的视图控制器即可。当然我们也可以让一个视图控制器分别在两个不同的场景里面使用,使用一个控制器时还需要在您的代码里面根据不同的场景做if,else的判断而使用两个控制器时则这些问题可以被规避,从而使得您的控制器代码更加清晰简单。

  • 两个功能界面中其中一个功能界面除了实现另外一个功能界面的所有能力外还有一些附加的功能
    对于新增能力的场景来说也是一样的,我们只需要在派生类中添加对应的附加界面和处理逻辑即可。考虑一个现实中的场景:在一般的电商类应用中每个商品都会有一个商品详情页面,这个商品详情一般从商品列表进入。当某个用户未登录时进去看到的商品详情只是普通的商品详情展示页面,而一旦登录后再进入这个商品详情页面时就有可能会在商品详情的某个部分比如底部出现这个用户对这个商品的购买记录信息。这个购买记录是和用户相关并且是可选的,而商品详情则和用户无关。我们在架构设计时就有可能设计出商品模块和用户模块两个部分。商品详情属于商品模块,它是独立于用户的,我们不可能在商品详情这个视图控制器中带上具有用户属性的一些界面和逻辑。解决的方法是我们建立一个商品详情视图控制器的派生类,然后在派生类面添加带有用户属性的东西比如用户的购买记录信息等。这样的设计思路也可以降低各个模块之间的耦合度。
//GoodsVC.h

//商品详情视图控制器
@interface  GoodsVC:UIViewController
@end

...............................................
//GoodsVC.m


@implementation GoodsVC

//这里的逻辑只是商品相关的逻辑,里面并不会涉及到任何用户相关的东西
@end

........................................
//GoodsWrapperVC.h

//带用户购买记录的商品详情视图控制器
@interface GoodsWrapperVC:GoodsVC

  -(id)initWithUser:(User*)user;

@end

.....................................
// GoodsWrapperVC.m

@interface GoodsWrapperVC()
    //用户购买记录列表
   @property(weak) UITableView *userRecordTableView;
 
   @property(strong) User *user;
   @property(strong) NSArray<Record*> records;
@end

@implementation GoodsWrapperVC

  -(id)initWithUser:(User*)user
 {
     self = [self init];
     if (self != nil)
     {
         _user = user;
     }
     return self;
 }

-(void)viewDidLoad
{
    [super viewDidLoad];

    //这里添加获取用户购买记录的请求逻辑。
    __weak  GoodsWrapperVC *weakSelf = self;
    [self.user getRecords:^(NSArray<Record*> *records, NSError *error{
         [weakSelf reloadRecordList:records];     
    }];
}

-(void)reloadRecordList:(NSArray<Record*>) *records
{
      //因为有些商品可能并无用户购买记录,所以这里特殊处理一下
      //用户购买记录列表也是可选并且是懒加载的,这样当商品详情并无用户购买记录时商品详情就和基类界面保持一致。
      if (records.count > 0)
      {
          self.records = records;
          if ( _userRecordTableView == nil)
          {
             UITableView *userRecordTableView = [[UITableView alloc] initWithFrame:CGRectMake(x,x,x,x)];
             userRecordTableView.delegate = self;
             userRecordTableView.dataSource = self;
             [self.view addSubview:userRecordTableView];
             _userRecordTableView = userRecordTableView;
         }
     }

    [self.userRecordTableView reloadData];
}

@end

.......................................
//GoodsListVC.m

//这里面是进入商品详情的商品列表视图控制器中的事件处理代码
-(void)handleShowGoodsDetail:(id)sender
{
      GoodsVC *goodsVC = nil;
     if (serviceSystem.user != nil && serviceSystem.user.isLogin)
     {
          goodsVC = [[GoodsWrapperVC alloc] initWithUser:serviceSystem.user];
     }
    else
    {
          goodsVC =[ [GoodsVC alloc] init];
    }

    [self.navigationController pushViewController:goodsVC animated:YES];
}

上面的进入商品详情的事件处理一般是在商品列表中进行,那我们又会面临同样的问题,就是商品列表其实和用户也是无关的,但是代码里面确出现了用户对象,这样就出现了商品模块和用户模块之间的耦合问题。怎么解决这个问题?答案就是路由,也就是我们在处理界面跳转时不直接构建目标视图控制器而是通过一个中介者路由来实现界面的跳转。关于路由来进行页面跳转的解决方案网络上已经有很多的开源库或者实现方式了,这里就不再赘述了。

视图的更新以及和数据模型的交互

最后我们再来说说令人烦恼的UITableViewCell的更新方法。UITableView是目前App中使用最多的控件之一。UITableViewCell是属于视图层次的对象。一般情况下某个UITableViewCell中展示的数据又来自于业务模型层的数据模型。更新一个UITableViewCell要做的事情其实就是将数据模型的变化反馈到视图中去,这里面同时涉及了视图和模型之间的耦合性问题。我们知道MVC中M和V之间是分别独立的,他们之间是通过C来建立关联,因此上面的UITableViewCell的更新就由视图控制器来完成。但是在实际中有可能UITableViewCell要显示的东西非常之多,而且展示的逻辑也比较复杂,如果这些代码都在视图控制器来处理的话那么势必造成控制器代码膨胀。怎么去解决这个问题也是我们这一小节要思考的问题。我将列出6种不同的解决方案来处理视图数据更新的问题:

  1. 视图提供属性
    这种方法是UITableViewCell默认的方法,在UITableViewCell中有imageVew、textLabel、detailTextLabel等几个默认的视图属性,一般情况下如果我们不定制UITableViewCell的话那么就可以在UITableView的delegate或者dataSource的回调处理中直接将数据模型的数据设置到这些属性上。同理如果我们要自定义UITableViewCell时我们也可以让UITableViewCell的派生类暴露出视图属性来解决问题。这种场景一般用于界面不复杂而且逻辑比较简单的情况。
//XXXTableViewCell.h

@interface XXXTableViewCell:UITableViewCell
  @property(weak) UILabel *nameLabel;
  @property(weak) UILabel *ageLabel;
  @property(weak) UILabel *addressLabel;
@end
 
  1. 视图暴露方法
    在一些应用场景中UITableViewCell中视图属性除了要更新内容外,显示的效果比如字体颜色等也有可能要更新。如果这部分逻辑特别多的话我们就考虑为UITableViewCell的派生类提供一个更新视图的方法来解决问题。通过提供方法的形式可以让我们的UITableViewCell不需要暴露里面的视图层次和视图属性给外面,提供的方法的参数都是一些数据即可,所有的视图更新和样式的设置都在方法内部完成,这样就可以减少在视图控制器中的代码量。也就是这种方法其实是将更新逻辑从视图控制器移到视图里面了。
//XXXTableViewCell.h

@interface XXXTableViewCell:UITableViewCell

//不再暴露视图属性了,但是提供一个更新视图的方法
-(void)update:(NSString*)name  age:(int)age  address:(NSString*)address;

@end

......................................
XXXTableViewCell.m

@interface XXXTableViewCell()
  @property(weak) UILabel *nameLabel;
  @property(weak) UILabel *ageLabel;
  @property(weak) UILabel *addressLabel;
@end

 @implementation XXXTableViewCell

-(void)update:(NSString*)name  age:(int)age  address:(NSString*)address
{
   // 这里将参数的内容更新到对应的子视图中去,并且这里面更新视图的显示样式等等。
  self.nameLabel.text = name;
  self.ageLabel.text = [NSString stringWithFormat:@"%d", age];
  self.addressLabel.text = address;
}
@end

..........................................

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
      XXXTableViewCell *cell = ( XXXTableViewCell *)[tableView dequeueReusableCellWithIdentifier:@"test" forIndexPath:indexPath];
     //这里面读取数据模型中的数据并调用视图的update来实现界面的更新。
     XXXDataModel *data = ....
     [cell update:data.name age:data.age address:data.address];
     return cell;
}

通过视图暴露更新方法的方案可以有效的减少视图控制器中的代码,而且可以隐藏视图更新的实现,但是缺点是当UITableViewCell的界面元素较多时则方法的参数将是非常的多。因此这个方法适合于界面元素不是很多的场景。

  1. 借助字典
    如果界面元素非常多时,但是我们又不想让视图和数据模型之间产生关联,那么我们可以将UITableViewCell中的update方法改造为只接收一个参数: 一个字典参数
   -(void)update:(NSDictionary*)params;

通过字典的形式来做数据的传递可以减少方法中参数的个数,而且现在也有非常多的将数据模型转化为字典的解决方案。采用字典作为参数时会增加数据转换的步骤,以及在UITableViewCell中的update方法一定要了解字典有哪些数据,并且外部调用时也要了解有哪些数据。在一定程度上字典的引入反而会使得代码的可维护性降低。

  1. 借助接口
    通过方法参数和字典是数据传递的两种不同的方式。缺点是一旦界面变化时都需要手动的调整参数位置和个数。当要更新的界面元素比较多时,我们还可以在更新方法中使用接口的形式来解决问题:
//一个独立的接口定义文件
//XXXXItf.h

@protocol  XXXXItf
 @property  NSString *name;
 @property  int age;
 @property  NSString *address;
@end


..............................
定义的数据模型实现接口
//XXXDataModel.h
#import "XXXXItf.h"

//数据模型实现接口
@interface XXXXDataModel:NSObject<XXXXItf>
 @property  NSString *name;
 @property  int age;
 @property  NSString *address;
@end

..................................
XXXXTableViewCell的定义
#import "XXXXItf.h"

@interface XXXXTableViewCell:UITableViewCell

//这里面的入参是一个接口协议。
-(void)update:(id<XXXXItf>)data;

@end

可以看出通过接口协议的形式可以解决方法参数过多以及字典作为参数的难维护性,通过接口定义的方法还可以解耦视图层和模型层之间的强关联问题。采用接口的方式的缺点就是需要额外的定义出一个接口协议出来。

  1. 视图持有模型
    通过接口协议可以解决视图和数据模型的耦合性,其实在实际中我们的某些UITableViewCell就是专门用于展示某种数据模型的,从某种程度上说他们之间其实是有非常强烈的耦合性的。因此这种情况下我们可以让这个UITableViewCell持有这个数据模型也未尝不是一个解决方案!!虽然MVC里面强调各个层次之间分离,但是在一些实际的场合中还是可以允许一些耦合场景出现的。当我们用视图持有数据模型时我们就可以不用提供一个update方法,而是直接将数据模型赋值给视图,视图内则可以重写数据模型属性的set方法来实现界面的更新。
//XXXXTableViewCell.h
#import "XXXXDataModel.h"

@interface XXXXTableViewCell:UITableViewCell

@property  XXXXDataModel *data;

@end

...........................
//XXXXTableViewCell.m

#import "XXXXTableViewCell.h"

@implementation XXXXTableViewCell

-(void)setXXXXDataModel:(XXXXDataModel*)data
{
     _data = data;
     //...这里更新界面的内容
}

@end

................................

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
      XXXTableViewCell *cell = ( XXXTableViewCell *)[tableView dequeueReusableCellWithIdentifier:@"test" forIndexPath:indexPath];
      cell.data =  ...   这里面将数据模型赋值给视图。
      return cell;
}


6.建立中间绑定类

上面的所有解决方案中要么就是将代码逻辑放在视图控制器中处理,要么就将代码逻辑移植到视图中处理,并且有可能视图还会持有数据模型的事情发生。我们还可以将这部分更新的逻辑提取出来让他即不在视图中处理也不在视图控制器中处理而是提供一个新的数据绑定类来解决这个问题。通过数据绑定类来实现视图和数据模型之间的交互也就是现在我们经常说道的MVVM中的VM类所做的事情。

//XXXXTableViewCell.h

@interface XXXXTableViewCell:UITableViewCell

//暴露出视图所具有的视图属性。
@property UILabel *nameLabel;
@property UILabel *addressLabel;

@end

...............................................
//XXXXDataModel.h

@interface XXXXDataModel:NSObject
@property NSString *name;
@property  NSString *address;
@end

.............................................
//XXXXViewModel.h

@interface XXXXViewModel:NSObject

-(id)initView:(XXXXTableViewCell*)cell  withData:(XXXXDataModel*)data;

@end

.......................................
//XXXXViewModel.m

@implementation XXXXViewModel

-(id)initView:(XXXXTableViewCell*)cell  withData:(XXXXDataModel*)data
{
    self = [self init];
    if (self != nil)
    {
         cell.nameLabel.text = data.name;
         cel.addressLabel.text = data.address;
    }
    return self;
}
@end

...................................................
//某个视图控制器

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
      XXXTableViewCell *cell = ( XXXTableViewCell *)[tableView dequeueReusableCellWithIdentifier:@"test" forIndexPath:indexPath];

      //假设这里取数据模型为data
      XXXXDataModel *data = ....
      //构建出一个视图模型绑定类。
      XXXXViewModel *vm = [ [XXXXViewModel alloc]   initView:cell withData:data];

      return cell;
}

上面的例子我们只是实现了一个简单的ViewModel类,他的作用非常的明确就是实现数据到视图之间的更新和绑定处理。从而使得视图部分的代码、视图控制器中的代码更加存粹和简单。缺点就是因为中间类的引入而使得代码增加和维护成本增加。

关于视图控制器的构建所要介绍的就是这些了,这又是一篇非常长的文章,而且还分为了上下两个部分,也许您不一定有耐心读完整个部分。但是我期望这些东西在您阅读后能让你对视图控制器和MVC有一个全新的认识。在编码前,无论工作量有多少,我们都应该要在提前有一个思路和思考。如何降低耦合性,如果使得我们的程序更加健壮和容易维护是我们思考的重点。在移动开发领域iOS和Android所提供给开发者的都是基于MVC的框架体系,这么多年来这种框架体系一直没有被改变那就证明他的生命还是比较顽强以及非常适合于目前移动开发。对于一个公司来说虽然开源的框架非常多,而且引入也非常容易,但是我们应该清醒的认识到,这些非官方的第三方库的引入一定要在你整个系统中的可替换性以及侵入性降到最低!而且越底层的部分对第三方的依赖一定要最低。所以在设计整个应用的架构时可替换性以及标准性应该成为重点要考虑的事情。


欢迎大家访问我的github地址简书地址

设计框架系列
Web note ad 1