iOS开发之MVVM+RAC架构模式

在说MVVM之前,首先要了解我们最常用的MVC设计模式⬇️

1.MVC设计模式

苹果官方将MVC设计模式作为iOS APP的标准模式

斯坦福大学公开课上的这幅图来说明,这可以说是最经典和最规范的MVC标准


MVC是最普遍认知的设计模式(高内聚,低耦合),MVC模式将页面的逻辑分为3块:Model(模型数据业务)、View(UI展示业务)、Controller(协调者-控制器)

Model(模型):是应用程序中用于处理应用程序数据逻辑的部分。
    通常模型对象负责在数据库中存取数据。

比如我们人类有一双手,一双眼睛,一个脑袋,没有尾巴,这就是模型,Model定义了这个模块的数据模型。
在代码中体现为数据管理者,Model负责对数据进行获取及存放。
数据不可能凭空生成的,要么是从服务器上面获取到的数据,要么是本地数据库中的数据,
也有可能是用户在UI上填写的表单即将上传到服务器上面存放,所以需要有数据来源。
既然Model是数据管理者,则自然由它来负责获取数据。
Controller不需要关心Model是如何拿到数据的,只管调用就行了。
数据存放的地方是在Model,而使用数据的地方是在Controller,
所以Model应该提供接口供controller访问其存放的数据(通常通过.h里面的只读属性)

View(视图):是应用程序中处理数据显示的部分。
    通常视图是依据模型数据创建的。

View,视图,简单来说,就是我们在界面上看见的一切。
它们有一部分是我们UI定死的,也就是不会根据数据来更新显示的,
比如一些Logo图片啊,这里有个按钮啊,那里有个输入框啊,一些显示特定内容的label啊等等;
有一部分是会根据数据来显示内容的,比如tableView来显示好友列表啊,
这个tableView的显示内容肯定是根据数据来显示的。
我们使用MVC解决问题的时候,通常是解决这些根据数据来显示内容的视图。

Controller(控制器):是应用程序中处理用户交互的部分。
    通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。

Controller是MVC中的数据和视图的协调者,也就是在Controller里面把Model的数据赋值给View来显示
(或者是View接收用户输入的数据然后由Controller把这些数据传给Model来保存到本地或者上传到
服务器)。

controller出现的原因:我们所有的App都是界面和数据的交互,所以需要类来进行界面的绘制,于是出现了View,需要类来管理数据于是出现了Model。我们设计的View应该能显示任意的内容比如页面中显示的文字应该是任意的而不只是某个特定Model的内容,所以我们不应该在View的实现中去写和Model相关的任何代码,如果这样做了,那么View的可扩展性就相当低了。而Model只是负责处理数据的,它根本不知道数据到时候会拿去干啥,可能拿去作为算法噼里啪啦去了,可能拿去显示给用户了,它既然无法接收用户的交互,它就不应该去管和视图相关的任何信息,所以Model中不应该写任何View相关代码。然而我们的数据和界面应该同步,也就是一定要有个地方要把Model的数据赋值给View,而Model内部和View的内部都不可能去写这样的代码,所以只能新创造一个类出来了,取名为Controller

2.MVC缺点

上述对于MVC的描述是理想状态下的MVC,Controller的作用应该是一个桥梁,建立起ModelView的连接,但实际开发中总是会出现Controller厚重,重量级控制器的问题,原因如下

  • 繁重的UI
  • 啰嗦的业务逻辑
  • 各种代理
  • 遗失的网络逻辑(无立足之地)
  • 较差的可测试性

在iOS开发中,UIKIt框架是将控制器Controller与View进行绑定了的,每个控制器都有View对象,代码添加UI子控件细节或者在xib与storyboard中子视图可以直接与controller进行关联,都会导致控制器中难以避免很多本该View去负责的UI子控件细节处理放在了控制器Controller里面;而在Controller里面本身要处理的请求、控制器生命周期函数要处理的事情比较多的情况下,控制器就变得很臃肿。实际上这个设计模式在iOS中为:M-VC

因此,M-VC 可能是对 iOS 开发中的 MVC模式更为准确的解读,同时更也准确地描述了我们日常开发可能已经编写的 MVC 代码,但它并没有做太多事情来解决 iOS 应用中日益增长的重量级视图控制器的问题。

iOS中的M-VC

举个例子: cell传值,就需要在VC里解析完数据在将model传给cell的view,增加了model和view之间的耦合,就变成了M-VC

这里还要特殊说一下那无处安放网络请求

苹果使用的MVC的定义是这么说的:所有的对象都可以被归类为一个model,一个view,或是一个controller。就这些。那么把网络代码放哪里?和一个API通信的代码应该放在哪儿?
你可能试着把它放在model对象里,但是也会很棘手,因为网络调用应该使用异步,这样如果一个网络请求比持有它的model生命周期更长,事情将变的复杂。显然也不应该把网络代码放在view里,因此只剩下controller了。这同样是个坏主意,因为这加剧了厚重View Controller的问题。
那么应该放在那里呢?显然MVC的3大组件根本没有适合放这些代码的地方。

3.MVVM设计模式

MVVM的诞生

就像我们之前分析MVC是如何合理分配工作的一样,我们需要数据所以有了M,我们需要界面所以有了V,而我们需要找一个地方把M赋值给V来显示,所以有了C,然而我们忽略了一个很重要的操作:数据解析。在MVC出生的年代,手机APP的数据往往都比较简单,没有现在那么复杂,所以那时的数据解析很可能一步就解决了,所以既然有这样一个问题要处理,而面向对象的思想就是用类和对象来解决问题,显然V和M早就被定义死了,它们都不应该处理“解析数据”的问题,理所应当的,“解析数据”这个问题就交给C来完成了。而现在的手机App功能越来越复杂,数据结构也越来越复杂,所以数据解析也就没那么简单了。如果我们继续按照MVC的设计思路,将数据解析的部分放到了Controller里面,那么Controller就将变得相当臃肿。还有相当重要的一点:Controller被设计出来并不是处理数据解析的。1、管理自己的生命周期;2、处理Controller之间的跳转;3、实现Controller容器。这里面根本没有“数据解析”这一项,所以显然,数据解析也不应该由Controller来完成。那么我们的MVC中,M、V、C都不应该处理数据解析,那么由谁来呢?这个问题实际上在面向对象的时候相当好回答:既然目前没有类能够处理这个问题,那么就创建一个新的类出来解决不就好了?所以我们聪明的开发者们就专门为数据解析创建出了一个新的类:ViewModel。这就是MVVM的诞生。

MVVVM解决的问题,你只需要记住两点:1、Controller的存在感被完全的降低了;2、VM的出现就是Controller存在感降低的原因。

Controller存在感降低的原因

在MVVM中,Controller不再像MVC那样直接持有Model了。想象Controller是一个Boss,数据是一堆文件(Model),如果现在是MVC,那么数据解析(比如整理文件)需要由Boss亲自完成,然而实际上Boss需要的仅仅是整理好的文件而不是那一堆乱七八糟的整理前的文件。所以Boss招聘了一个秘书,现在Boss就不再需要管理原始数据(整理之前的文件)了,他只需要去找秘书:你帮我把文件整理好后给我。那么这个秘书就首先去拿到文件(原始数据),然后进行整理(数据解析),接下来把整理的结果给Boss。所以秘书就是VM了,并且Controller(Boss)现在只需要直接持有VM而不需要再持有M了。如果再进一步理解C、VM、M之间的关系:因为Controller只需要数据解析的结果而不关心过程,所以就相当于VM把“如何解析Model”给封装起来了,C甚至根本就不需要知道M的存在就能把工作做好,前提它需要持有一个VM。那么我们MVVM中的持有关系就是:C持有VM,VM持有M。这里有一个比较争议的地方:C该不该持有M。我的答案是不该。为什么呢,因为C持有M没有任何意义。就算C直接拿到了M的数据,它还是要去让VM进行数据解析,而数据解析就需要M,那么直接让VM持有M而C直接持有VM就足够了。最后再分享一个我在实现MVVM中的一个技巧,也谈不上是技巧吧,算是一种必要的思想:一旦在实现Controller的过程中遇到任何跟Model(或者数据)相关的问题,就找VM要答案。这个思想待会我们会在实现代码的时候用到。

MVVM双向绑定的实现

使用block和监听实现该功能

ViewController核心代码

@property (nonatomic, strong) NSMutableArray *dataArray;
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) MVVMViewModel *vm;

- (void)viewDidLoad {
    [super viewDidLoad];

    [self.view addSubview:self.tableView];
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    
    self.vm = [[MVVMViewModel alloc] init];
    __weak typeof(self) weakSelf = self;
    //model -> UI -> 代码块
    [self.vm initWithBlock:^(id data) {
        //获取到更新后的数据,刷新UI
        NSArray *array = data;
        [weakSelf.dataArray removeAllObjects];
        [weakSelf.dataArray addObjectsFromArray:array];
        
        MVVMView *view = [[MVVMView alloc]initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, (array.count + 1)/4*50)];
        [view headViewWithData:array];
        weakSelf.tableView.tableHeaderView = view;
        [weakSelf.tableView reloadData];
    } WithErrorBlock:^(id errorCode) {
        
    }];
    
    [self.vm reloadData];
}
#pragma mark - tableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    self.vm.contenKey = self.dataArray[indexPath.row];   
    //这里通过修改MVVMViewModel的contenKey来更新数据(触发通知方法),更新好的数据在通过block回调到VC中,完成了MVVM的双向绑定
}

ViewModel核心代码

//.h代码
#import 
#import 
#import "ViewModelClass.h"   //父类为封装好方便调用block的基类,具体可参照demo
@interface MVVMViewModel : ViewModelClass
@property (nonatomic, copy) NSString *contenKey;  //通过KVO监听该值,当该值发生改变后更新数据,回调VC

- (void)reloadData;  
@end
//.m代码
#import "MVVMViewModel.h"
@implementation MVVMViewModel

- (instancetype)init{
    if (self == [super init]) {
        [self addObserver:self forKeyPath:@"contenKey" options:NSKeyValueObservingOptionNew context:nil];
    }
    return self;
}

- (void)reloadData{
    //数据一般为网络获取,获取后进行回调,VC刷新页面
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSArray *array = @[@"转账",@"信用卡",@"充值中心",@"蚂蚁借呗",@"电影票",@"滴滴出行",@"城市服务",@"蚂蚁森林"];
        dispatch_async(dispatch_get_main_queue(), ^{
            if (self.successBlock) {
                //返回block
                self.successBlock(array);
            }
        });
    });
}

#pragma mark - KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    NSLog(@"%@",change);    //通过监听得知数据变化
    //处理数据
    NSArray *array = @[@"转账",@"信用卡",@"充值中心",@"蚂蚁借呗",@"电影票",@"滴滴出行",@"城市服务",@"蚂蚁森林"];
    NSMutableArray *mArray = [NSMutableArray arrayWithArray:array];
    [mArray removeObject:change[NSKeyValueChangeNewKey]];
    //双向绑定通知VC更新变化后的数据
    if (self.successBlock) {
        self.successBlock(mArray);
    }
}

- (void)dealloc
{
    [self removeObserver:self forKeyPath:@"contenKey"];  //移除通知
}

@end

实际开发中,我们可以搭配RAC使用MVVM的架构模式,代码更简洁、易懂

4.ReactiveCocoa简介

ReactiveCocoa是响应式编程(FRP)在iOS和OS中的一个实现框架,它的开源地址为:https://github.com/ReactiveCocoa/ReactiveCocoa

优点

RAC虽然最大的优点是提供了一个单一的、统一的方法去处理异步的行为,包括delegate方法、blocks回调、target-action机制、notificationsKVO

详细来说,在iOS开发过程中,当某些事件响应的时候,需要处理某些业务逻辑,这些事件都用不同的方式来处理。
比如按钮的点击使用action,ScrollView滚动使用delegate,属性值改变使用KVO等系统提供的方式。
其实这些事件,都可以通过RAC处理
ReactiveCocoa为事件提供了很多处理方法,而且利用RAC处理事件很方便,可以把要处理的事情,和监听的事情的代码放在一起,这样非常方便我们管理,就不需要跳到对应的方法里。非常符合我们开发中高聚合,低耦合的思想。

集成RAC

通过pods集成,集成步骤可以参考这两篇文章
ios 通过CocoaPods安装第三方库
Xcode如何集成Pod教程
ps:该三方库支持多个平台iOS、OS、Swift,iOS集成pod 'ReactiveObjC', '~> 3.1.0'

RAC常用语法
  • UITextField
    @weakify(self);
    [[self.testTextFileld rac_textSignal] subscribeNext:^(NSString * _Nullable x) {
    @strongify(self);
        NSLog(@"%@",x);
        self.testTextFileld.text = @"Hello";
    }];

监听了输入框内所有的变化,包括准备编辑,和退出编辑。再也不用写delegate了,编码起来方便快捷!!!

  • UIButton
    [[self.btn rac_signalForControlEvents:(UIControlEventTouchUpInside)] subscribeNext:^(__kindof UIControl * _Nullable x) {
        NSLog(@"%@",[x class]);
    }];

平常写按钮的触发事件都要新建一个方法去实现,现在不用了,直接在你的按钮下面写实现的代码。实例化和触发事件写在一起,查阅代码和维护代码更加直观!!!

  • NSNotificationCenter
    [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil] subscribeNext:^(NSNotification * _Nullable x) {
        NSLog(@"%@",x);
    }];

还能监听通知的各种事件,上面就是监听了APP退到后台的事件。最重要的一点就是不需要移除通知,比通知用起来更爽,无后顾之忧!!!

MVVM+RAC代码

只需要把上述代码的init方法修改为RAC代码即可

- (instancetype)init{
    if (self == [super init]) {
        //订阅信号(热信号!!)    监听contenKey值的变化
        [RACObserve(self, contenKey) subscribeNext:^(id  _Nullable x) {
            NSArray *array = @[@"转账",@"信用卡",@"充值中心",@"蚂蚁借呗",@"电影票",@"滴滴出行",@"城市服务",@"蚂蚁森林"];
            NSMutableArray *mArray = [NSMutableArray arrayWithArray:array];
            [mArray removeObject:x];
            if (self.successBlock) {
                self.successBlock(mArray);
            }
        }];
    }
    return self;
}

上述代码已经实现了MVVM+RAC的开发,那么我们项目中的MVC架构模式应该如何去优化呢?

5.如何对 ViewController 瘦身?

  • 将网络请求抽象到单独的类中
  • 将界面的拼装抽象到专门的类中
  • 创建构造类似 ViewModel 的工厂类,参见 工厂模式。另外,也可以专门将数据存取都抽将到一个 Service 层,由这层来提供 ViewModel 的获取。

6.MVVM 存在的问题

  • 数据绑定使得 Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得一个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。
  • 对于过大的项目,数据绑定需要花费更多的内存。
  • 存在一定的学习成本和引入更多的三方库(RAC等等),代码逻辑更复杂

7.总结

  • MVC的设计模式也并非是病入膏肓,无药可救的架构,最起码目前MVC设计模式仍旧是iOS开发的主流框架,存在即合理。针对文章所述的弊端,我们依旧有许多可行的方法去避免和解决,从而打造一个轻量级的ViewController

  • MVVMMVC的升级版,完全兼容当前的MVC架构,MVVM虽然促进了UI 代码与业务逻辑的分离,一定程度上减轻了ViewController的臃肿度,但是ViewViewModel之间的数据绑定使得MVVM变得复杂和难用了,如果我们不能更好的驾驭两者之间的数据绑定,同样会造成Controller 代码过于复杂,代码逻辑不易维护的问题。

  • 一个轻量级的ViewController是基于MVCMVVM模式进行代码职责的分离而打造的。MVCMVVM有优点也有缺点,但缺点在他们所带来的好处面前时不值一提的。他们的低耦合性,封装性,可测试性,可维护性和多人协作便利大大提高了开法效率。

  • 同时,我们需要保持的是一个拥抱变化的心,以及理性分析的态度。在新技术的面前,不盲从,也不守旧,一切的决策都应该建立在认真分析的基础上,这样才能应对技术的变化。

  • 在选择架构的方向上,请记住这句话:没有最好的架构,只有最适合的

Demo下载:
https://github.com/gaoyuGood/MVVM-RAC

参考文献:
iOS 关于MVC和MVVM设计模式的那些事
mvc和mvvm的区别
浅谈 MVC、MVP 和 MVVM 架构模式
被误解的 MVC 和被神化的 MVVM
iOS开发之RAC(一)初级篇
最快让你上手ReactiveCocoa之基础篇

你可能感兴趣的:(iOS开发之MVVM+RAC架构模式)