ReactiveCocoa(RAC)-iOS

作者: PHM | 来源:发表于2016-08-31 23:20 被阅读8344次

    简介


    ReactiveCocoa(简称为RAC),RAC具有函数响应式编程特性,由Matt Diephouse开源的一个应用于iOS和OS X的新框架。

    为什么使用RAC?


    因为RAC具有高聚合低耦合的思想所以使用RAC会让代码更简洁,逻辑更清晰。

    如何在项目中添加RAC?


    platform:ios, '7.0'
    pod 'ReactiveCocoa','~>2.0'

    • 其他方法看最下方官方链接

    工作原理


    工作原理

    常见类解释


    1. Stream - 信号流值 - RACStream类
    表示一个基本单元可以为任意值,其值会随着事件的变化而变化,可以在其上进行一些复杂的操作运算(map,filter,skip,take等.)此类不会被经常使用, 多情况下表现为signal和sequences(RACSignal 和RACSequence继承于RACStream类)

    [[RACObserve(self, reactiveString)
        filter:^BOOL(NSString *value) {
            return [value hasPrefix:@"A"];
    }]
    subscribeNext:^(NSString *value) {
            NSLog(@"%@",value);
    }];
    

    2. Signals - 信号 - RACSignal类

    RACSignal能力

    什么是Signals?


    Signals

    有订阅者监听时信号才会发信息, Signals会向那个订阅者发送0或多个载有数值的”next”事件,后面跟着一个”complete”事件或一个”error”事件。
    Signals会发送三种不同信号给Subscriber

    • next:是可以为nil的新值, RACStream方法只能在这个值上进行操作运算。
    • error:表示在Signals完成之前发生了错误,值不会在RACStream类中存储。
    • completed:表示Signals成功的完成,值不会在RACStream类中存储。
    订阅者监听
    __block int aNumber = 0;
    // Signal that will have the side effect of incrementing `aNumber` block
    // variable for each subscription before sending it.
    RACSignal *aSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
        aNumber++;
        [subscriber sendNext:@(aNumber)];
        [subscriber sendCompleted];
        return nil;
    }];
            
    // This will print "subscriber one: 1"
    [aSignal subscribeNext:^(id x) {
        NSLog(@"subscriber one: %@", x);
    }];
            
    // This will print "subscriber two: 2"
    [aSignal subscribeNext:^(id x) {
        NSLog(@"subscriber two: %@", x);
    }];
    

    如果需要对信号进行过滤,转换,分解和合并那些值的话则不同的订阅者可能需要使用信号通过不同方式发送的值。


    信号处理
    RACSignal *usernameIsValidSignal = RACObserve(self.viewModel, usernameValid);
    RAC(self.Button, alpha) = [usernameIsValidSignal
        map:^(NSNumber *valid) {
            return valid. boolValue?@1:@0.5;
    }];
    

    3. Subscriber - 订阅者 - RACSubscriber协议
    表示能够接收信号的对象,订阅信号才会激活信号,实现RACSubscriber协议的对象都可以为订阅者。
    可以通过- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock 订阅信号。

    RACSignal *repeatSignal = [[RACSignal interval:1 onScheduler:[RACScheduler mainThreadScheduler]] repeat];
    [repeatSignal subscribeNext: ^(NSDate* time){
          NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
          [formatter setDateFormat:@"HH:mm:ss"];
          NSLog(@"%@",[formatter stringFromDate:time]);
    }];
    

    4. Subjects - 手动控制信号 - RACSubject
    表示可以手动控制信号,
    处理流程:创建信号-订阅信号-发送信号

    // 1.创建信号
    RACSubject *subject = [RACSubject subject];
    // 2.订阅信号 First
    [subject subscribeNext:^(id x) {
        // block调用时刻:当信号发出新值,就会调用.
          NSLog(@"FirstSubscribeNext%@",x);
    }];
    // 2.订阅信号 Second
    [subject subscribeNext:^(id x) {
          // block调用时刻:当信号发出新值,就会调用.
          NSLog(@"SecondSubscribeNext%@",x);
    }];
    // 3.发送信号
    [subject sendNext:@"1"];
    [subject sendNext:@"2"];
    

    也是RAC代码与非RAC代码的Bridge 所以非常有用,此类继承于RACSignal类。

    5. ReplaySubject - 手动回放控制信号 - RACReplaySubject
    表示可以手动控制信号,底层实现和RACSubject不一样,它会先把值保存起来,然后遍历刚刚保存的所有订阅者,一个一个调用订阅者的nextBlock然后调用subscribeNext订阅信号,遍历保存的所有值,一个一个调用订阅者的nextBlock。
    可以有以下两种处理流程:

    处理流程 1:创建信号-订阅信号-发送信号(和Subjects一样)
    处理流程 2:创建信号-发送信号-订阅信号

    // 1.创建信号
    RACReplaySubject *replaySubject = [RACReplaySubject subject];
    // 2.发送信号
    [replaySubject sendNext:@"1"];
    [replaySubject sendNext:@"2"];
    // 3.订阅信号 First
    [replaySubject subscribeNext:^(id x) {
          NSLog(@"FirstSubscribeNext%@",x);
    }];
    // 3.订阅信号 Second
    [replaySubject subscribeNext:^(id x) {
          NSLog(@"SecondSubscribeNext%@",x);
    }];
    

    6. Command- 命令信号 - RACCommand
    表示订阅响应Action信号,通常由UI来出发,比如一个Button当控件被触发时会被自动禁用掉。

    UIButton *reactiveBtn = [[UIButton alloc] init];
    [reactiveBtn setTitle:@"点我" forState:UIControlStateNormal];
        reactiveBtn.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(UIButton *input) {
        NSLog(@"点击了我:%@",input.currentTitle);
        //返回一个空的信号量
        return [RACSignal empty];
    }];
    

    7. Sequences- 集合 - RACSequence
    表示一个不可变的序列值且不能包含空值,使用-rac_sequence.signal来获取Signal。

    RACSignal *signal = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;
    // Outputs
    [signal subscribeNext:^(NSString *x) {
        NSLog(@"%@", x);
    }];
    

    8. Disposables- 清理订阅 - RACDisposable
    表示用于取消信号的订阅,当一个signal被subscriber后,当执行sendComplete或sendError时subscriber会被移除,或者手动调用[disposable dispose]进行移除操作。
    当subscriber被移除后,所有该subscriber相关的工作都会被停止或取消,如http请求,资源也会被释放。

    9. Scheduler- 计划 - RACScheduler
    表示一个信号队列,是信号执行任务时所在的队列或者信号执行完成后将结果放到队列里执行,它支持取消对列里的执行并总是串行执行。

    RAC常用宏


    RACObserve(TARGET, KEYPATH)
    表现形式:RACObserve(self, stringProperty)
    KVO的简化版本 相当于对TARGET中KEYPATH的值设置监听,返回一个RACSignal

    RAC(TARGET, ...)
    表现形式:RAC(self, stringProperty) = TextField.rac_textSignal
    第一个是需要设置属性值的对象,第二个是属性名
    RAC宏允许直接把信号的输出应用到对象的属性上
    每次信号产生一个next事件,传递过来的值都会应用到该属性上

    RACChannelTo(TARGET, ...)
    RACChannelTo 用于双向绑定
    RACChannelTo(self, stringProperty)=RACChannelTo(self.label, text) ;

    RAC结构图


    RAC结构图

    RAC基础使用


    创建一个TextField名为usernameTextField 设置监听TextField

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

    filter:如果想添加一个条件 只输出x的长度大于3的,可以使用filter操作来实现这个目的

    [self.usernameTextField.rac_textSignal
        filter:^BOOL(NSString* text){
        return text.length > 3;
    }];
    
    filter

    map:把text转换成length进行输出,使用map可以对信号进行转换,一个源信号转换成另外一个新的信号输出

    [[[self.usernameTextField.rac_textSignal
    map:^id(NSString*text){
      return @(text.length);
    }]
    filter:^BOOL(NSNumber*length){
      return[length integerValue] > 3;
    }]
    subscribeNext:^(id x){
      NSLog(@"%@", x);
    }];
    
    map

    信号可聚合也可以分割

    聚合: 多个信号可以聚合成一个新的信号,这个可以是任何类型的信号

    RACSignal *signal =
    [RACSignal combineLatest:@[validUsernameSignal, validPasswordSignal]
                        reduce:^id(NSNumber*usernameValid, NSNumber *passwordValid){
                          return @([usernameValid boolValue]&&[passwordValid boolValue]);
                        }];
    

    分割:一个信号可以有很多subscriber,也就是作为很多后续步骤的源

    RACSignal *signal = self.usernameTextField.rac_textSignal;
        [signal subscribeNext:^(id x) {
            NSLog(@"1111");
        }];
        [signal subscribeNext:^(id x) {
            NSLog(@"2222");
        }];
    }
    

    RAC设置Button的ControlEvents

    [[self.signInButton
         rac_signalForControlEvents:UIControlEventTouchUpInside]
         subscribeNext:^(id x) {
         NSLog(@"button click");
    }];
    
    rac_signalForControlEvents
    登陆功能举例说明
    需要实现登陆功能需要点击登陆button
    - (RACSignal *)signInSignal {
        return [RACSignal createSignal:^RACDisposable *(id subscriber){
         [self.signInService 
         signInWithUsername:self.usernameTextField.text
                   password:self.passwordTextField.text
                   complete:^(BOOL success){
                        [subscriber sendNext:@(success)];
                        [subscriber sendCompleted];
           }];
         return nil;
        }];
    }
    
    [[[[self.signInButton
         rac_signalForControlEvents:UIControlEventTouchUpInside]
         doNext:^(id x){
           self.signInButton.enabled =NO;
           self.signInFailureText.hidden =YES;
         }]
    
        flattenMap:^id(id x){        
            return[self signInSignal];
        }]
    
        subscribeNext:^(NSNumber*signedIn){
        self.signInButton.enabled =YES;
        BOOL success =[signedIn boolValue];
        self.signInFailureText.hidden = success;
        if(success){
            [self performSegueWithIdentifier:@"signInSuccess" sender:self];
        }
    }];
    

    flattenMap:[self signInSignal]返回的也是signal,所以是信号中的信号,使用这个操作把按钮点击事件转换为登录信号,同时还从内部信号发送事件到外部信号。

    doNext:为一个附加操作,在一个next事件发生时执行的逻辑,而该逻辑并不改变事件本身。

    流程

    RAC高级使用


    error 和 completed,节流,线程,延伸,其他

    内存管理

    ReactiveCocoa设计的一个目标就是支持匿名生成管道这种编程风格。到目前为止,在你所写的所有响应式代码中,这应该是很直观的。
    为了支持这种模型,ReactiveCocoa自己持有全局的所有信号。如果一个signal有一个或多个订阅者,那这个signal就是活跃的。如果所有的订阅者都被移除了,那这个信号就能被销毁了。

    如何取消订阅一个signal?
    在一个completed或者error事件之后,订阅会自动移除。你还可以通过RACDisposable 手动移除订阅。

    RACSignal的订阅方法都会返回一个RACDisposable实例,它能让你通过dispose方法手动移除订阅。这个方法并不常用到,但是还是有必要知道可以这样做。

    RACSignal *backgroundColorSignal =
      [self.searchText.rac_textSignal 
          map:^id(NSString *text) { 
              return [self isValidSearchText:text] ? 
                  [UIColor whiteColor] : [UIColor yellowColor]; 
      }]; 
    
    RACDisposable *subscription = 
    [backgroundColorSignal 
        subscribeNext:^(UIColor *color) {
            self.searchText.backgroundColor = color; 
    }]; 
    
    [subscription dispose];​
    

    避免循环引用
    在ReactiveCocoa中提供了避免循环引用的方法
    @weakify宏让你创建一个弱引用的影子对象(如果你需要多个弱引用,你可以传入多个变量),
    @strongify让你创建一个对之前传入@weakify对象的强引用。

    @weakify(self) 
    [[self.searchText.rac_textSignal 
    map:^id(NSString *text) { 
        return [self isValidSearchText:text] ? 
            [UIColor whiteColor] : [UIColor yellowColor]; 
    }] 
    subscribeNext:^(UIColor *color) { 
        @strongify(self) 
        self.searchText.backgroundColor = color; 
    }];​
    

    signal能发送3种不同类型的事件
    Next
    Completed
    Error

    当应用获取访问社交媒体账号的权限时,用户会看见一个弹框。这是一个异步操作,因此把这封装进一个signal是很好的选择

    -(RACSignal *)requestAccessToTwitterSignal {
    // 1 - define an error 
    NSError *accessError = [NSError errorWithDomain:RWTwitterInstantDomain 
                                               code:RWTwitterInstantErrorAccessDenied 
                                           userInfo:nil];
                                       
    // 2 - create the signal 
    @weakify(self) 
    return [RACSignal createSignal:^RACDisposable *(id subscriber) { 
        // 3 - request access to twitter 
        @strongify(self) 
        [self.accountStore requestAccessToAccountsWithType:self.twitterAccountType 
               options:nil 
            completion:^(BOOL granted, NSError *error) {
            // 4 - handle the response 
            if (!granted) { 
               [subscriber sendError:accessError]; 
            } else { 
                [subscriber sendNext:nil]; 
                [subscriber sendCompleted]; 
            } 
        }]; 
    return nil; 
    }]; 
    }​
    

    then:then方法会等待completed事件的发送,然后再订阅由then block返回的signal。这样就高效地把控制权从一个signal传递给下一个。

    [[[[self requestAccessToTwitterSignal] 
    then:^RACSignal *{ 
        @strongify(self) 
        return self.searchText.rac_textSignal; 
    }] 
    filter:^BOOL(NSString *text) { 
        @strongify(self) 
        return [self isValidSearchText:text]; 
    }] 
    subscribeNext:^(id x) { 
        NSLog(@"%@", x); 
    } error:^(NSError *error) { 
        NSLog(@"An error occurred: %@", error); 
    }];​
    
    then

    实时搜索内容方法

    • 创建请求链接方法
    -(SLRequest *)requestforTwitterSearchWithText:(NSString *)text { 
    NSURL *url = [NSURL URLWithString:@"https://api.twitter.com/1.1/search/tweets.json"]; 
    NSDictionary *params = @{@"q" : text}; 
    SLRequest *request = [SLRequest   requestForServiceType:SLServiceTypeTwitter 
                                        requestMethod:SLRequestMethodGET 
                                                  URL:url 
                                           parameters:params]; 
    return request; 
    }​
    
    • 创建请求signal
    -(RACSignal *)signalForSearchWithText:(NSString *)text { 
    // 1 - define the errors 
    NSError *noAccountsError = [NSError errorWithDomain:RWTwitterInstantDomain 
                                                   code:RWTwitterInstantErrorNoTwitterAccounts 
                                               userInfo:nil]; 
    NSError *invalidResponseError = [NSError errorWithDomain:RWTwitterInstantDomain 
                                                        code:RWTwitterInstantErrorInvalidResponse 
                                                        userInfo:nil]; 
                                                        
    // 2 - create the signal block 
    @weakify(self) 
    return [RACSignal createSignal:^RACDisposable *(id subscriber) { 
        @strongify(self); 
        
        // 3 - create the request 
        SLRequest *request = [self requestforTwitterSearchWithText:text]; 
        
        // 4 - supply a twitter account 
        NSArray *twitterAccounts = [self.accountStore accountsWithAccountType:self.twitterAccountType];       
        if (twitterAccounts.count == 0) { 
            [subscriber sendError:noAccountsError]; 
        } else { 
            [request setAccount:[twitterAccounts lastObject]]; 
            
        // 5 - perform the request 
        [request performRequestWithHandler: ^(NSData *responseData, 
                NSHTTPURLResponse *urlResponse, NSError *error) { 
            if (urlResponse.statusCode == 200) { 
            
                // 6 - on success, parse the response 
                NSDictionary *timelineData = [NSJSONSerialization JSONObjectWithData:responseData 
                                                options:NSJSONReadingAllowFragments 
                                                  error:nil]; 
                [subscriber sendNext:timelineData]; 
                [subscriber sendCompleted]; 
            } else { 
                // 7 - send an error on failure 
                [subscriber sendError:invalidResponseError]; 
            } 
        }]; 
    } 
    return nil; 
    }];
    }
    
    • 使用flattenMap来把每个next事件映射到一个新的signal
    [[[[[self requestAccessToTwitterSignal] 
    then:^RACSignal *{ 
        @strongify(self) 
        return self.searchText.rac_textSignal; 
    }] 
    filter:^BOOL(NSString *text) { 
        @strongify(self) 
        return [self isValidSearchText:text]; 
    }] 
    flattenMap:^RACStream *(NSString *text) { 
        @strongify(self) 
        return [self signalForSearchWithText:text]; 
    }] 
    subscribeNext:^(id x) { 
        NSLog(@"%@", x); 
    } error:^(NSError *error) { 
        NSLog(@"An error occurred: %@", error); 
    }];
    

    线程

    在subscribeNext:error:中的数据没有在主线程(Thread 1)中执行,更新UI只能在主线程中执行,所以更新UI需要转到主线程中执行。

    要怎么更新UI呢?
    通常的做法是使用操作队列但是ReactiveCocoa有更简单的解决办法,在flattenMap:之后添加一个deliverOn:操作就可以转到主线程上了。
    :如果你看一下RACScheduler类,就能发现还有很多选项,比如不同的线程优先级,或者在管道中添加延迟。

    [[[[[[self requestAccessToTwitterSignal] 
    then:^RACSignal *{ 
        @strongify(self) 
        return self.searchText.rac_textSignal; 
    }] 
    filter:^BOOL(NSString *text) { 
        @strongify(self) 
        return [self isValidSearchText:text]; 
    }] 
    flattenMap:^RACStream *(NSString *text) { 
        @strongify(self) 
        return [self signalForSearchWithText:text]; 
    }] 
    deliverOn:[RACScheduler mainThreadScheduler]] 
    subscribeNext:^(id x) { 
        NSLog(@"%@", x); 
    } error:^(NSError *error) { 
        NSLog(@"An error occurred: %@", error); 
    }];
    

    异步加载图片

    -(RACSignal *)signalForLoadingImage:(NSString *)imageUrl { 
    RACScheduler *scheduler = [RACScheduler 
        schedulerWithPriority:RACSchedulerPriorityBackground]; 
        
    return [[RACSignal createSignal:^RACDisposable *(id subscriber) { 
        NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]]; 
        UIImage *image = [UIImage imageWithData:data]; 
        [subscriber sendNext:image]; 
        [subscriber sendCompleted]; 
        return nil; 
    }] subscribeOn:scheduler]; 
    }
    

    首先获取一个后台scheduler,来让signal不在主线程执行。然后,创建一个signal来下载图片数据,当有订阅者时创建一个UIImage。最后是subscribeOn:来确保signal在指定的scheduler上执行。

     -(UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
    UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"TableViewCell"];
    [[[[self signalForLoadingImage:tweet.profileImageUrl] 
    takeUntil:cell.rac_prepareForReuseSignal] 
    deliverOn:[RACScheduler mainThreadScheduler]] 
    subscribeNext:^(UIImage *image) { 
        cell.twitterAvatarView.image = image; 
    }];
    return cell;
    }
    

    cell是重用的,可能有脏数据,所以上面的代码首先重置图片。然后创建signal来获取图片数据。你之前也遇到过deliverOn:这一步,它会把next事件发送到主线程,这样subscribeNext:block就能安全执行了。

    cell.rac_prepareForReuseSignal:Cell复用时的清理。
    takeUntil:当给定的signal完成前一直取值

    节流

    每次输入一个字,搜索都会马上执行。如果你输入很快(或者只是一直按着删除键),这可能会造成应用在一秒内执行好几次搜索,这很不理想。
    更好的解决方法是,当搜索文本在短时间内,比如说500毫秒,不再变化时,再执行搜索。
    在filter之后添加一个throttle步骤:

    [[[[[[[self requestAccessToTwitterSignal] 
    then:^RACSignal *{ 
        @strongify(self) 
        return self.searchText.rac_textSignal; 
    }] 
    filter:^BOOL(NSString *text) { 
        @strongify(self) 
        return [self isValidSearchText:text]; 
    }] 
    throttle:0.5] 
    flattenMap:^RACStream *(NSString *text) { 
        @strongify(self) 
        return [self signalForSearchWithText:text]; 
    }] 
    deliverOn:[RACScheduler mainThreadScheduler]] 
    subscribeNext:^(NSDictionary *jsonSearchResult) { 
        NSArray *statuses = jsonSearchResult[@"statuses"]; 
        NSArray *tweets = [statuses linq_select:^id(id tweet) { 
            return [RWTweet tweetWithStatus:tweet]; 
        }]; 
        [self.resultsViewController displayTweets:tweets]; 
    } error:^(NSError *error) { 
        NSLog(@"An error occurred: %@", error); 
    }];
    

    throttle:只有当前一个next事件在指定的时间段内没有被接收到后,throttle操作才会发送next事件。

    代替代理

    如果想在其他地方监听到tableView的代理信息则需要设置如下方法

    [[tableView rac_signalForSelector:@selector(tableView:didSelectRowAtIndexPath:) fromProtocol:@protocol(UITableViewDelegate) ] subscribeNext:^(RACTuple * x) {
        NSLog(@"点击了");
    }];
    

    rac_signalForSelector: fromProtocol: 要先绑定在设置代理

    测试Demo


    ReactiveCocoaTest

    官方链接地址


    ReactiveCocoa开源地址
    官方开发使用文档-Swift
    官方开发使用文档-OC

    扩展阅读


    ReactiveCocoa 基本用法
    说说ReactiveCocoa 2
    学习RAC小记-适合给新手看的RAC用法总结
    ReactiveCocoa-tutorial-pt1
    ReactiveCocoa-tutorial-pt2
    MVVC-RAC

    相关文章

      网友评论

      • Resory:博主你好,
        “可以通过- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock 方法创建Subscriber。"
        关于这句话感觉不太妥,此方法激活信号,内部实现是会创建一个Subscriber,但是这个方法的返回值并不是一个Subscriber。
        PHM:👌 谢谢提醒 修改一下
      • Vijay_:谢谢大神分享

      本文标题:ReactiveCocoa(RAC)-iOS

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