MVVM+RAC项目实战用法

前言

因为公司项目的原因,开始接触MVVM+RAC的这种模式,刚开始并不是很适应这种函数式响应式的编程思想,感觉使用起来非常繁琐,大大的增加了开发的负担.但是随着自己学习的深入和项目的实践,这种模式的优点也随之显现.所以写这篇文章希望记录自己学习的过程,如果有写的不对的地方也希望大家指正.

本篇文章主要针对的是Objective-C语言来讲解ReactiveCocoa的应用,使用的也是公认最稳定的ReactiveCocoa v2.5,ReactiveCocoa在3.0以后的版本就是针对Swift的版本,所以大家可以根据自己需要来做下载.

目录

  • 1:MVVM由来
  • 2:RAC浅析
  • 3:实战使用

一:MVVM由来

大家都知道MVC是iOS App推荐的用来组织代码的权威规范,大部分的App也都遵循这样的构建,但是这样的设计模式却会随着项目的不断发展,业务逻辑的不断复杂让Controller变得臃肿,使得MVC从Model View Controller变成了Massive View Controller,这时传统的MVC设计模式已经不能满足我们的需求.而MVVM的出现极大的解决了这一问题,他是MVC的进一步发展,将Controller里面的业务逻辑全部抽离到ViewModel里面,我们只需要在Controller里面处理逻辑的回调结果即可.

当然MVVM使我们的Controller完成了瘦身,但是ViewModel的出现,也使得我们需要在Controller中引入ViewModel这个类,使得我们所管理的类又多了一个,之间的交互就变得更加的麻烦.此时RAC的出现就正好接管这一套逻辑上的交互,用“信号流”的概念使得逻辑变得扁平化,我们只要关心“信号流”的流向即可.

二:RAC浅析

RAC 中最核心的概念之一就是信号RACStream,RACStream中包含的两个子类——RACSignal 和 RACSequence.因为本篇文章只是介绍RAC在实战中的用法,所以会以RACSignal来介绍(好吧,其实是因为笔者了解的太浅了-u-).如果想知道具体内部实现可以去看下霜神关于RAC源码解读的文章.

1:RACSignal

不说废话了,直接开干,首先来看一段我们常见的signal创建->订阅->销毁信号的整个流程代码.

     //创建信号
 RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id subscriber) {
    //发送信号
    [subscriber sendNext:@"啊哈啦啦啦"];
    [subscriber sendCompleted];

    //取消订阅 可以选择在此做资源释放的操作
    return [RACDisposable disposableWithBlock:^{
        NSLog(@"signal dispose");
    }];
}];

RACDisposable *disposable = [signal subscribeNext:^(id x) {
    NSLog(@"subscribe value = %@",x);
    //输出结果: subscribe value = 啊哈啦啦啦
}];

   //取消订阅
[disposable dispose];

内部逻辑实现:
(1):RACSingal调用createSignal:创建信号,内部会去调用其子类RACDynamicSignal去创建信号.
(2):RACDynamicSignal调用createSignal:方法,后面唯一的参数是个叫didSubscribe的block,当执行sendNext发送信号时,会将发送的内容保存在didSubscribe的block中.
(3):signal信号执行subscribeNext方法,会把之前保存在didSubscribe的内容取出来.
(4):取消订阅,执行disposableWithBlock这个block.

这样RACSignal的创建->订阅->销毁信号的一整个流程代码就完成了.这种只有当订阅者完成了订阅才会发送信号,所以我们称其为冷信号.他就像是在一条生产线上,打开了机器,但是这个时候没有工人上班,那么工厂也不会正常运作.

2:RACSubject

通过查看源码我们发现RACSubject是继承自RACSignal的一个子类,并且遵循了<RACSubscriber>协议,意味着它既可以订阅信号,也能发送信号.

RACSubject的例子应用

//调用subject方法创建信号
 RACSubject *subject = [RACSubject subject]; 
 //订阅信号
   [subject subscribeNext:^(id x) {
    NSLog(@"x = %@",x);    
   }];
  //发送信号
 [subject sendNext:@"啊哈啦啦啦"];

内部实现逻辑:
(1):调用subject方法,创建信号.内部创建一个_subscribers可变数组,用来存储订阅信号的订阅者.
(2):调用sendNext方法,发送消息.这时内部调用enumerateSubscribersUsingBlock方法对订阅者进行遍历,并发送消息.
(3):所有订阅过该subject信号的订阅者会收到此消息,并完成打印x内容.

到这里RACSubject的一整套流程就完成了. RACSubject中不管有没有信号被订阅它都会去发送消息,这种特性的信号我们称之为热信号.就好比工厂里的生产线一直在运作,有工人订阅了就会用数组存起来,等到有任务(消息)下发了,就会去执行这个任务.

3:RACCommand

查看源码我们知道,RACCommand和之前的“信号流”概念不太一样,它是一个继承自NSObject的类,它的主要目的是为了管理和订阅RACSignal的类.在我们做UI组件交互的时候, RACCommand能够帮助我们更快的处理业务,降低代码的复杂度,节省开发的时间.

使用场景:监听按钮的点击事件、网络请求与回调处理.

知道了RACCommand的用途和使用场景,为了更好的理解RACCommand,我们先来看看RACCommand的两个初始化方法和执行方法:

初始化方法:

- (id)initWithSignalBlock:(RACSignal * (^)(id input))signalBlock;
- (id)initWithEnabled:(RACSignal *)enabledSignal signalBlock:(RACSignal * (^)(id input))signalBlock;

执行方法:

- (RACSignal *)execute:(id)input;
- (void)setRac_command:(RACCommand *)command;

方法介绍:
1:我们知道RACCommand的作用是管理RACSignal的信号,所以初始化方法的signalBlock的返回类型就是我们需要管理的RACSignal,他的入参input,就是我们执行该Command时所传入的数据.
2:初始化第二个方法较第一个方法多了个RACSignal类型的参数enabledSignal;这个参数的目的主要是为了过滤信号,只有当该信号中传递的参数为真时, Command才能够被执行.
3:执行方法中第一个方法是RACCommand里面用于执行的方法,直接调用即可.
4:执行方法中第二个方法是UIButton的分类方法,具体使用后面会做介绍.

知道了MVVM、RACSingal、RACSubject、RACCommand的介绍和用法,接下来我们就可以在实际项目中进行应用了.

三:实战使用

首先我们需要在ViewModel.h文件中声明一个command,用于管理我们的信号.

//声明属性testCommand
@property (nonatomic, strong) RACCommand *testCommand;

注意:这里声明的testCommand必须使用strong修饰强引用,否则接受不到RACCommand内部的信号.

然后ViewModel.m文件中在get方法中进行初始化操作.

//testCommand
- (RACCommand *)testCommand
{
    if (!_testCommand) {
    
      _testCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) {
        
          return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            
              //需要传递的参数,如果传入的input就是网络请求需要的参数,直接传input即可
              NSDictionary *sendParams = @{@"test":@"我是啊哈啦啦啦"};
            
              //在这里面进行网络请求的操作,笔者自己把网络请求封装成了一个信号,方便订阅处理.
             [[YWApiManager sendApi:SCApiTypeTest withParam:sendParams] subscribeNext:^(NSDictionary *json{
                  //json:是网络请求回调后,转换后取得的json
                  [subscriber sendNext:json];
                 //一定要加上sendCompleted这个方法,不然无法再次执行该command
                  [subscriber sendCompleted];
                
              } error:^(NSError *error) {
               //错误信息 sendError 内部已经取消订阅信号 不用执行sendCompleted方法
                  [subscriber sendError:error];
              }];
            
             return nil;
      
         }];
        
     }];
    
 }
      return _testCommand;
}

上面的方法中,笔者直接采用了initWithSignalBlock这个方法初始化RACCommand,如果说你在执行方法时已将需要需要传递的参数字典传入,那么可以直接将input当成sendParams传入.
注意:在发送消息后,一定要执行[subscriber sendCompleted]; 表示发送消息已经结束,取消信号的订阅.不然的话该command会一直处于执行中,不能再次执行该command.

写到这里 已经成功的将我们Controller中的网络请求和回调处理好了,接下来我们需要在Controller里面对信号发送的json进行处理,看下面Controller中的代码.

首先我们需要在Controller中导入ViewModel,并且声明对象viewModel.具体操作看下面的代码

[self.viewModel.testCommand.executionSignals subscribeNext:^(RACSignal * _Nullable execution) {
    
    [execution subscribeNext:^(id x) {
        //x为网络请求的回调结果,可以在这里对数据进行处理
        NSLog(@"json = %@",x);
    }];
    
}];

使用testCommand的executionSignals信号进行订阅操作. executionSignals是一个内部装有RACSignal的高阶信号,所以我们对他进行降阶操作拿到execution信号,并再次订阅此信号,此时入参的x就是我们之前传递的网络请求回调“json”.

如果我们在非并发RACCommand中我们可以用switchToLatest进行降阶操作,这样写比较直观,也是笔者在项目中常用的方法.

[self.viewModel.testCommand.executionSignals.switchToLatest subscribeNext:^(id x) {
    //x为网络请求的回调结果,可以在这里对x做处理,修改UI
    NSLog(@"json = %@",x);
}];

对于错误信号的订阅:

[self.viewModel.testCommand.errors subscribeNext:^(NSError *error) {
    NSLog(@"error = %@",error);
}];

注意:我们不应该使用subscribeError:这个方法取订阅错误信号,因为executionSignals这个信号是不会发送error事件的.所以需使用subscribeNext:订阅错误信号.

最后我们只要执行该方法就行了,执行代码地方传的参数可以为空,或者传入需要用到的参数,这个可以根据需求自己来决定.

 [self.viewModel.testCommand execute:@"啊哈啦啦啦"];

然后我们再来看看第二种执行方法,这种方法会使按钮绑定上testCommand,如果RACCommand是以initWithEnabled这种方式初始化的,按钮的enabled属性会随enabledSignal传入的值的改变而改变.即传入值为真,按钮不可点击.

 testButton.rac_command = self.viewModel.testCommand;

以上代码都是针对冷信号来处理,让我们在看下RACSubject在项目中的用法.

应用场景:比如现在我们有个tableView的列表,每个cell的点击事件跳到新的界面,在新的界面中我们会选择一些数据并回传到之前的界面,最后刷新tableView,把选择的数据展示在tableView上.这样的一个操作我们就可以使用RACSubject来完成,看下面代码.

 @property (nonatomic, strong) RACSubject *reloadSignal;

首先我在ViewModel里面声明了一个热信号reloadSignal,然后初始化testCommand.

 //testCommand
- (RACCommand *)testCommand
{
     if (!_testCommand) {
         @weakify(self);
     _testCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) {
        
        return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            
                [subscriber sendNext:@"我是阿哈啦啦啦,我要被发送了"];
                [subscriber sendCompleted];
                return nil;
            
            }]doNext:^(id x) {
               @strongify(self);
               //doNext的入参x是sendNext发送的参数
               [self.reloadSignal sendNext:x];
            
           }];
        
        }];
    
    }
      return _testCommand;
}

和第一个例子不同的是,这里发送的数据会进行一步操作,调用doNext:方法(将sendNext的参数传递给doNext的入参).然后热信号reloadSignal发送入参x.

最后在Controller中的操作,和例1中是一样的,要注意的是调用时需要使用reloadSignal进行订阅.热信号的优点在于,对于需要进行多次reload的这种操作,我们不用去重复订阅.

 [self.viewModel.reloadSignal subscribeNext:^(id x) {
    NSLog(@"x = %@",x);
}];

最后

关于RAC这块笔者自己还在学习之中,所以希望抛砖引玉,大家互相讨论共同进步.以上就是关于ReactiveCocoa的一个简单用法,比较简单实用,希望能帮到新学习RAC的各位.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354