美文网首页ReactiveCocoa
iOS ReactiveCocoa框架的简单使用

iOS ReactiveCocoa框架的简单使用

作者: 爬树的蚂蚁 | 来源:发表于2017-02-26 18:15 被阅读1461次

    ReactiveCocoa框架的使用教程在网上有很多详细的博客可参考,通过学习,我自己也整理了一下,一来便于自己复习,二来分享给大家。先粘贴些优质博文的链接,然后下面以实例的形式一步步讲解。

    优质技术博客链接

    地址1:ReactiveCocoa入门教程——第一部分
    地址2:ReactiveCocoa入门教程——第二部分
    地址3:1个小时学会ReactiveCocoa基本使用

    下面开始介绍ReactiveCocoa的使用

    一、在项目中集成ReactiveCocoa框架

    既然是第三方框架,那用CocoaPods集成是最方便的。
    首先,创建一个工程
    然后,在工程中创建Podfile文件,文件中的内容如下:
    platform :ios,'9.0'
    use_frameworks!
    target 'RWReactivePlayground' do
    pod 'ReactiveCocoa', '~> 2.5'
    end

    注意1:

    集成ReactiveCocoa框架和其他的不同之处是多了一个“use_frameworks!”,我在使用过程中发现,2.5版本以上的更高的版本要加上“use_frameworks!”,否则会报错,导致集成不了。而2.5版本之前的(包括2.5版本),就不需要加“use_frameworks!”,

    注意2:

    ReactiveCocoa现在的最高版本已经到5.0了,问题是,如果用swift编程,那么集成最新版本的ReactiveCocoa框架没有问题,但是如果使用OC编程的话,那最高只能集成2.5版本的RAC(RAC是ReactiveCocoa的简称),否则集成好了以后工程会报错。

    简单的说就是,如果你用swift编程,用Cocoapods集成时,Podfile文件这么写
    platform :ios,'9.0'
    use_frameworks!
    target 'RWReactivePlayground' do
    pod 'ReactiveCocoa', '~> 5.0'
    end
    如果你用的是oc编程,用Cocoapods集成时,Podfile文件这么写
    platform :ios,'9.0'
    target 'RWReactivePlayground' do
    pod 'ReactiveCocoa', '~> 2.5'
    end
    最后,上面的工作都做好了,就可以集成RAC了,很快.


    屏幕快照 2017-02-27 上午10.59.13.png
    二、RAC的简单使用----RACSignal

    在要使用的RAC的控制器中导入RAC框架的头文件

    #import <ReactiveCocoa/ReactiveCocoa.h>
    

    现在来熟悉下RACSignal的使用,从名字就可以看出,它是信号。在viewDidload中加入下面的代码

    //创建信号
        RACSignal * single = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            NSLog(@"想");
            [subscriber sendNext:@"发送了信号"];//发送信号
            NSLog(@"你");
            [subscriber sendCompleted];//发送完成,订阅自动移除
            //RACDisposable 可用于手动移除订阅
            return [RACDisposable disposableWithBlock:^{
                NSLog(@"豆腐");
            }];
        }];
        //订阅信号
        NSLog(@"我");
        [single subscribeNext:^(id x) {
            NSLog(@"吃");
    //        NSLog(@"信号的值:%@",x);
        }];
    

    运行,得到结果如下


    RACSinale.png

    这样就可以清楚的看明白,信号的运行流程,但是感觉好乱,下面分析一下:
    1.createSignal方法 是创建信号,创建好的信号,没有被订阅前,只是冷信号,此时是不会走createSignal后面的block的。
    程序往下,就走到“NSLog(@"我")”,

    2.然后走到subscribeNext,这一步就是订阅信号,订阅号信号后,信号single就变成了热信号,

    3.既然变成热信号,就开始走createSignal后面的block中的去,所以就打印出了“NSLog(@"想")”。

    4.下面是sendNext,即发送信号,发送了信号,订阅者就会收到信号,发送的内容可以从订阅信号subscribeNext后面的block中获取到,程序就走到subscribeNext后面的block中,所以就打印了“NSLog(@"吃")”,

    5.当订阅信号的subscribeNext后面的block走完以后,程序又回到,createSignal后面的block中,继续未完成的代码,所以就打印“NSLog(@"你")”,继续往下就是[subscriber sendCompleted],这句代码的意思是,发送完成了,订阅自动移除,没有了订阅者了,信号又变成了冷信号。

    6.接下来就是return,返回一个RACDisposable对象,这个的作用就是,可以用来手动移除订阅。RACDisposable对象,创建完成,就走进创建方法的block中,也就是打印NSLog(@"豆腐")

    综上,打印出来的结果就是“我想吃你豆腐”,它就是这样出来的

    这里再介绍下RACDisposable的使用,将代码改一下

        //创建信号
        RACSignal * single = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            NSLog(@"想");
            [subscriber sendNext:@"发送了一个信号"];//发送信号
            NSLog(@"你");
            //RACDisposable 手动移除订阅者
            return [RACDisposable disposableWithBlock:^{
                NSLog(@"豆腐");
            }];
        }];
        //订阅信号
        NSLog(@"我");
        RACDisposable * disposable = [single subscribeNext:^(id x) {
            NSLog(@"吃");
            NSLog(@"信号的值:%@",x);
        }];
        //手动移除订阅
        [disposable dispose];
    

    打印结果如下



    在稍微分析一下,两份代码不同之处是,删去了自动移除订阅[subscriber sendCompleted],添加了手动删除订阅[disposable dispose],手动删除订阅,可以在你想要的地方,合适的时候进行操作。不过手动删除用的少。那既然用得少,我们还是用自动删除吧,优化下,见代码

    //创建信号
        RACSignal * single = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            NSLog(@"想");
            [subscriber sendNext:@"发送了信号"];//发送信号
            NSLog(@"你");
            [subscriber sendCompleted];//发送完成,订阅自动移除
            //RACDisposable 手动移除订阅者
            return nil;
        }];
        //订阅信号
        NSLog(@"我");
        [single subscribeNext:^(id x) {
            NSLog(@"吃");
            NSLog(@"信号的值:%@",x);
        }];
    

    打印结果如下



    好了,没豆腐吃了!其实也不需要。返回nil就可以了

    上面罗里吧嗦的说了那么多,就是为了理清里面的逻辑,没有结合实际使用,其实听起来还是很迷糊,下面就结合实际,来使用RAC

    第二、RAC的常用方法

    上面是使用RACSignal创建信号,其实文本框中文字改变也是信号,按钮点击也是信号,RAC为UITxtField和UIButton创建categary,并做好了封装,直接就可以调用它们的信号,这里就围绕着这两个类,进行ARC的使用讲解
    在工程中新建一个控制器,添加几个控件,textField,textView,button,label,如下图

    订阅textField信号

    [self.textfield.rac_textSignal subscribeNext:^(id x) {
            NSLog(@"%@",x);
        }];
    

    因为textField的信号肯定是NSString,类型的,所以可以写成下面的样子,也更方便使用些

    [self.textfield.rac_textSignal subscribeNext:^(NSString* x) {
            NSLog(@"%@",x);
        }];
    

    这样,当你在文本框输入时,控制台就会打印出输入的内容,如下


    可以看到,每次输入都会获取到信号。

    filter---信号过滤器

    如果我只需要将字符串长度超过3的,才打印,那可以使用过滤器filter,使用方法如下

    [[self.textfield.rac_textSignal filter:^BOOL(NSString* value) {
            return value.length>3?YES:NO;
        }]subscribeNext:^(NSString* x) {
            NSLog(@"过滤后的到的信号:%@",x);
        }];
    

    在文本框中输入字符,打印结果如下



    可以看到,只有当字符串长度大于3的信号,才会被订阅到

    map--转换器

    map就是将一种信号转换成你想要的另一种信号,这里把字符串信号,转换成文字信号
    如果想当文本框中输入的文字长度大于4的时候,改变文本框的背景色,一种方法是把过滤器的条件设置为4,然后在subscribeNext的block中直接给textField.backgroundColor赋值。不过RAC有转换信号的方法---map,如下

    [[[self.textfield.rac_textSignal filter:^BOOL(NSString* value) {
            return value.length>3?YES:NO;
        }]map:^id(NSString* value) {
            return value.length > 4?[UIColor redColor]:[UIColor whiteColor];
        }]subscribeNext:^(UIColor* value) {
            self.textfield.backgroundColor = value;
        }];
    

    上面的代码的意思是,当输入的字符串长度超过3,就将字符串信号转换成颜色信号,然后订阅该颜色信号,并将颜色赋值给textField的背景色。效果如下



    这样的话,当字符串大于3,文本框的背景色变成了红色
    另外,RAC提供了一个宏"RAC(对象,属性)"来简化代码并增强可读性,如下

    RAC(self.textfield ,backgroundColor) = [self.textfield.rac_textSignal map:^id(NSString* value) {
            return value.length > 4?[UIColor redColor]:[UIColor whiteColor];
        }];
    

    RAC宏有两个参数,一个是需要设置的对象,一个是设置的属性。这句代码的意思是,当文本框输入的字符串长度大于4时,改变文本框的背景色。这样的话,看起来更清晰,而达到的效果是一样的。

    总结一下,到现在为止,学了过滤器:filter,转换器:map,对象设置属性的宏:RAC(要设置的对象,要设置的属性)。可以想象,用这几个方法可以很方便的实现一些功能,比喻说替代通知,监听事件等。

    textField的使用是这样,那textView的使用也是这样的,因为他们完全类似

    RAC(self.textView ,backgroundColor) = [self.textView.rac_textSignal map:^id(NSString* value) {
            return value.length > 4?[UIColor redColor]:[UIColor whiteColor];
        }];
    
    combineLatest:reduce:

    想象一下,如果当textField和textView同时满足某个条件时,才能进行某项操作的话,应该如何写呢?RAC为我们准备了一个方法--combineLatest:reduce:信号合并
    先看代码

    RACSignal * mergeTwoSignal = [RACSignal combineLatest:@[self.textfield.rac_textSignal,self.textView.rac_textSignal] reduce:^id(NSString * value1,NSString * value2){
            return [NSNumber numberWithBool:([value1 isEqualToString:@"11111"]&&[value2 isEqualToString:@"22222"])];
        }];
    RAC(self.addButton,enabled) = [mergeTwoSignal map:^id(NSNumber* value) {
            return value;
        }];
    

    上面的代码的意思是,当textField中的文字为"11111",同时textView中的文字为"22222"的时候,返回一个信号,信号的类型是NSNumber,然后通过转换器map,将值返回,返回的值用于确定按钮是否可用。
    可能会疑问,map中返回的NSNumber类型的,而button的enabled属性是BOOL类型,怎么可以这样直接赋值,但是RAC它就是可以,就是做的这么好。
    到这一步,就可以订阅button的点击信号了,看代码就懂了

    [[self.addButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
         subscribeNext:^(NSNumber * value) {
             //标签赋值
             self.displayLabel.text = @"1314";
         }];
    

    运行验证一下,结果如下



    确实能达到要求。真好,再也不用给button 添加点击事件了。

    doNext

    现在讲一下附加操作doNext,它的作用是,在不改变信号的基础上,进行一些附加的操作,比喻说,我在订阅到给label赋值前,改变label的背景色,当然也可以是做别的操作。反正是附加的不会影响信号流的。使用见代码

    [[[self.addButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
         doNext:^(id x) {
             //改变label的背景色
             self.displayLabel.backgroundColor = [UIColor redColor];
         }]
         subscribeNext:^(NSNumber * value) {
             self.displayLabel.text = @"1314";
         }];
    

    这样就实现了,订阅信号前,改变label的背景色

    @weakify和@strongify

    RAC的所有方法中,大部分是block,所以无法避免在使用过程中导致循环引用,
    以前的解决办法是这样的

        __weak SecondViewController *bself = self;
        [[[self.addButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
            doNext:^(id x) {
                //先清掉label中的文字
                bself.displayLabel.textColor = [UIColor redColor];
            }]
           subscribeNext:^(NSNumber * value) {
               bself.displayLabel.text = @"1314";
           }];
    

    如果每个block都写的话,会很费劲,因为block太多了,还好RAC提供了两个宏,@weakify和@strongify,

        @weakify(self);
        [[[self.addButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
          doNext:^(id x) {
              @strongify(self);
              self.displayLabel.textColor = [UIColor redColor];
          }]
         subscribeNext:^(NSNumber * value) {
             @strongify(self);
             self.displayLabel.text = @"1314";
         }];
    

    @weakify宏让你创建一个弱引用的影子对象(如果你需要多个弱引用,你可以传入多个变量),@strongify让你创建一个对之前传入@weakify对象的强引用。这样就解决了循环引用的问题

    第三、RAC在网络请求和图片加载中的使用

    先创建一个控制器,添加若干控件,textView,用来展示请求到的数据,imageView,用来展示图片,

    使用系统的方法请求数据

    在viewDidload中添加下面的代码

        NSURL * url = [NSURL URLWithString:urlS];
        NSURLSession * session = [NSURLSession sharedSession];
        NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:url];
        NSURLSessionTask * task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            NSString * dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            NSLog(@"%@",dataString);
            NSDictionary * dic = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
            NSLog(@"%@",dic);
            [self performSelector:@selector(actionWithString:) onThread:[NSThread mainThread] withObject:dataString waitUntilDone:YES];
        }];
        [task resume];
    
    //回到主线程给textView赋值
    -(void)actionWithString:(id )value{
        self.textView.text = (NSString*)value;
    }
    

    结果如下


    使用RAC请求网络数据

    把系统请求网络数据的方法,封装成信号流

    //rac网络请求
    -(RACSignal *)racNetworkRequest{
        return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            NSURL * url = [NSURL URLWithString:urlS];
            NSURLSession * session = [NSURLSession sharedSession];
            NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:url];
            NSURLSessionTask * task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
                NSString * dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    //            NSLog(@"%@",dataString);
    //            NSDictionary * dic = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
    //            NSLog(@"%@",dic);
                if (error ==nil) {//返回成功
                    [subscriber sendNext:dataString];//发送信号
                    [subscriber sendCompleted];//结束发送
                } else {
                    [subscriber sendError:error];//发送错误
                }
            }];
            [task resume];
            return nil;
        }];
    }
    

    现在就来调用一下看看,在viewDidLoad中添加下面的代码,
    self.requestDataButton是一个按钮,使用方法是点击按钮的时候加载数据

        [[[self.requestDataButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
        map:^id(id value) {
            return [self racNetworkRequest];
        }]
        subscribeNext:^(id x) {
            NSLog(@"%@",x);
        }];
    

    订阅信号后,得到的数据如下



    发现,得到的不是想要的数据,而是一个信号对象,其实从racNetworkRequest这个方法中就可以看出,返回就是一个RACSignal对象,如果能获取到RACSignal对象里面的信号流就对了,怎么办呢,RAC提供了这样的方法 flattenMap

    flattenMap---获取信号中的信号

    把上面的代码写成这样,就可以获取到数据了

        [[[self.requestDataButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
          flattenMap:^id(id value) {
              return [self racNetworkRequest];
          }]
         subscribeNext:^(id x) {
             NSLog(@"%@",x);
         } error:^(NSError *error) {
             NSLog(@"%@",error);
         }];
    

    这样就会发现订阅到的信号,是你想要的数据了。

    then---等待上一个信号的完成,然后订阅自己的信号
        [[[self racNetworkRequest]
        then:^RACSignal *{
            return self.textField.rac_textSignal;
        }]
        subscribeNext:^(id x) {
            NSLog(@"%@",x);
        }error:^(NSError *error) {
            NSLog(@"error");
        }];
    

    then方法会等待前面的信号中completed事件的发送完成,然后再订阅由then block返回的信号。这样就高效地把控制权从一个signal传递给下一个。如此就实现了:当请求数据完成,就可以监控到textField中的文字输入了

    回到主线程---deliverOn

    因为信号的流转及操作都是在block中完成的,也就是说大部分操作都是在子线程中执行的操作,但是有个时候需要回到主线程完成一些事情,比如,请求到数据后,要刷新UI,这就必须回到主线程,RAC提供了这样的方法deliverOn。用法见下面的代码

        @weakify(self)
        [[[[[self racNetworkRequest]
            then:^RACSignal *{
                @strongify(self);
                return self.textField.rac_textSignal;
            }]
           filter:^BOOL(NSString* value) {
               return value.length > 3?YES:NO;
           }]
        deliverOn:[RACScheduler mainThreadScheduler]]//回到主线程
        subscribeNext:^(NSString * value) {
             @strongify(self);
             self.textView.text =value;
             NSLog(@"%@",value);
            NSLog(@"当前线程%@",[NSThread currentThread]);
        } error:^(NSError *error) {
             NSLog(@"%@",error);
         }];
    

    这样的话,实现的效果就是,在textField中输入文字而且当文字大于3的时候,会在textView中显示出来,而且可以看到订阅信号的block中打印出来的线程是主线程,如下:



    悲催的是,如果没有加deliverOn:好像也是在主线程。我也不知道什么原因,不知道有没有用,姑且就认为deliverOn有用,可能在开启很多线程的时候会有用吧
    不过我可以在subscribeNext的block中加入回到主线程的方法,也能达到目的,如下

        subscribeNext:^(NSString * value) {
             @strongify(self);
             self.textView.text =value;
             NSLog(@"%@",value);
            [self performSelectorOnMainThread:@selector(doSomething) withObject:nil waitUntilDone:YES];
            NSLog(@"当前线程%@",[NSThread currentThread]);
        } error:^(NSError *error) {
             NSLog(@"%@",error);
         }];
    
    信号节流---throttle

    用文本框textField作比喻,当我在里面输入字符时,subscribeNext的block会不停的走,每输入一个字符,就会走一遍。如果我想在输入过程中不需要每改变一个字符就走一遍,而是等输入完成或停止的时候再走block里面的代码,那就可以用throttle,先看效果

        [[[self.textField.rac_textSignal
           filter:^BOOL(NSString* value) {
               return value.length > 3?YES:NO;
           }]
          throttle:1]
         subscribeNext:^(NSString * value) {
             @strongify(self);
             self.textView.text =value;
         } error:^(NSError *error) {
             NSLog(@"%@",error);
         }];
    

    这样得到的效果是,当输入字符串长度大于3,而且该字符串的值在1s内没有改变,就把textField中的值,赋值给textView。所以简单的说throttle的作用:如果前面信号在设定的时间内没有变化时,throttle就会把信号传到下面的事件中去。

    使用系统的方法加载图片

    系统的方法,我就不说了 看代码

        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_async(queue, ^{
            NSURL * url = [NSURL URLWithString:imageUrlString];
            UIImage * image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
            if (image!=nil) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    self.imageView.image = image;
                });
            }
        });
    

    这样就可以完成图片的加载,看下面的效果


    RAC加载图片

    创建一个加载图片的方法,方法返回的是RACSignle信号对象,

    -(RACSignal*)racRequestImage{
        return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            NSURL * url = [NSURL URLWithString:imageUrlString];
            UIImage * image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
            self.imageView.image = image;
            [subscriber sendNext:image];
            [subscriber sendCompleted];
            return nil;
        }];
    }
    

    下面就来调用这个方法。实现的效果是,点击按钮(self.requestImageDataButton),即开始加载图片,在viewDidLoad中添加下面的代码

        @weakify(self);
        [[[self.requestImageDataButton rac_signalForControlEvents:(UIControlEventTouchUpInside)]
           flattenMap:^RACStream *(id value) {
               return [self racRequestImage];
           }]
        deliverOn:[RACScheduler mainThreadScheduler]]
        subscribeNext:^(UIImage * image) {
            @strongify(self);
             self.imageView.image = image;
         }];
    

    运行一下,发现,图片加载正常。从代码量来看,GCD可能方便一点,但是,如果是多个事件凑到一起影响图片加载的时候,RAC或许是不错的选择。

    到这一步,就把ReactiveCocoa的初步使用讲完了。
    总结一下总共学习哪些方法

    filter---信号过滤器
    map--转换器
    combineLatest:reduce:--信号合并
    doNext--附加操作
    @weakify和@strongify--避免循环
    flattenMap---获取信号中的信号
    then---等待上一个信号的完成,然后订阅自己的信号
    回到主线程---deliverOn
    信号节流---throttle
    第四、demo下载

    GitHub下载地址

    相关文章

      网友评论

        本文标题:iOS ReactiveCocoa框架的简单使用

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