RAC学习笔记1·

RAC支持的UI控件

UI信号支持
MKAnnotationView + RACSignalSupport  
UIActionSheet+RACSignalSupport  
UIAlertView+RACSignalSupport   
UICollectionReusableView+RACSignalSupport
UIControl+RACSignalSupport
UIControl+RACSignalSupportPrivate
UIDatePicker+RACSignalSupport
UIGestureRecognizer+RACSignalSupport
UIImagePickerController+RACSignalSupport
UISegmentedControl+RACSignalSupport
UISlider+RACSignalSupport
UIStepper+RACSignalSupport
UISwitch+RACSignalSupport
UITableViewCell+RACSignalSupport
UITableViewHeaderFooterView+RACSignalSupport
UITextField+RACSignalSupport
UITextView+RACSignalSupport

***RACCommandSupport 类支持
UIBarButtonItem+RACCommandSupport
UIButton+RACCommandSupport
UIRefreshControl+RACCommandSupport
RACCommand

RACCommand类用于表示事件的执行,一般来说是在UI上的某些动作来触发这些事件,比如点击一个按钮。RACCommand的实例能够决定是否可以被执行,这个特性能反应在UI上,而且它能确保在其不可用时不会被执行。通常,当一个命令可以执行时,会将它的属性allowsConcurrentExecution设置为它的默认值:NO,从而确保在这个命令已经正在执行的时候,不会同时再执行新的操作。命令执行的返回值是一个RACSignal,因此我们能对该返回值进行next:,completed或error
http://blog.csdn.net/womendeaiwoming/article/details/37597779

学习笔记第一部分

第一部分来源: http://www.cocoachina.com/ios/20150123/10994.html
第二部分来源: http://benbeng.leanote.com/post/ReactiveCocoaTutorial-part2
RAC进阶教程: http://www.devtoutiao.com/7779

  • ReactiveCocoa signal(RACSignal)发送事件流给它的subscriber。目前总共有三种类型的事件:next、error、completed。一个signal在因error终止或者完成前,可以发送任意数量的next事件。第一部分,我们将会关注next事件。下一部分error和completed事件。
    //  打印用户输入的值
    [self.textField.rac_textSignal subscribeNext:^(id x) {
      NSLog(@"%@",x);
    }];
        // 当输入字符长度大于3时,才开始打印log
        [[self.textField.rac_textSignal filter:^BOOL(id value) {
            NSString *text = value;
            return text.length > 3;
        }]subscribeNext:^(id x) {
            NSLog(@"%@",x);
        }];

     上面所创建的只是一个很简单的管道。这就是响应式编程的本质,根据数据流来表达应用的功能。
     rac_textSignal  ---> filter ---> subscribeNext
                            |- length > 3
     rac_textSignal是起始事件。然后数据通过一个filter,如果这个事件包含一个长度超过3的字符串,那么该事件就可以通过。管道的最后一步就是subscribeNext:,block在这里打印出事件的值。
     只要数据流改变,就会发送信号
分开来写
      RACSignal *userNameFieldSignal = self.textField.rac_textSignal;
       // 筛选事件,也是一个信号
       RACSignal *filterUserName = [userNameFieldSignal filter:^BOOL(id value) {
           NSString *text = value;
           return text.length > 3;
       }];
       [filterUserName subscribeNext:^(id x) {
           NSLog(@"%@",x);
       }];
RACSignal的每个操作都会返回一个RACsignal,这在术语上叫做连贯接口(fluent interface)。这个功能可以让你直接构建管道,而不用每一步都使用本地变量。
 注意:ReactiveCocoa大量使用block。如果你是block新手,你可能想看看Apple官方的block编程指南。如果你熟悉block,但是觉得block的语法有些奇怪和难记,你可能会想看看这个有趣又实用的网页f*****gblocksyntax.com
RAC_MAP(映射) FILTER(筛选)
   [[[self.textField.rac_textSignal map:^id(NSString *text) {
       return @(text.length);
    }] filter: ^BOOL(NSNumber *length) {
       return [length integerValue] > 3;
    }]subscribeNext:^(id x) {
        NSLog(@"%@",x);
    }];

此时log输出变成了文本的长度而不是内容
新加的map(映射)操作通过block改变了事件的数据。map从上一个next事件接收数据,通过执行block把返回值传给下一个next事件。在上面的代码中,map以NSString为输入,取字符串的长度,返回一个NSNumber。
rac_textSignal ---> map ---> filter ---> subscribeNext
| | |
|- NSString -| - - - - - - NSNumber - - - - >|
注意:在上面的例子中text.length返回一个NSUInteger,是一个基本类型。为了将它作为事件的内容,NSUInteger必须被封装。幸运的是Objective-C literal syntax提供了一种简单的方法来封装——@ (text.length)。

上面的代码对每个输入框的rac_textSignal应用了一个map转换。输出是一个用NSNumber封装的布尔值。
下一步是转换这些信号,从而能为输入框设置不同的背景颜色。基本上就是,你订阅这些信号,然后用接收到的值来更新输入框的背景颜色。下面有一种方法:

  // 转换背景颜色 - (不推荐使用此种方式)
  [[passwordSignal map:^id(NSNumber *passwordValid) {
       return [passwordValid boolValue]?[UIColor lightGrayColor]:[UIColor yellowColor];
  }]subscribeNext:^(UIColor  *bgColor) {
      self.password.backgroundColor = bgColor;
   }];

//改变背景颜色 推荐使用的代码 RAC宏方式改变背景色。编译运行,可以发现当输入内容无效时: 输入框置灰 - 有效时 : 高亮.

    RACSignal *userNameSignal = [self.textField.rac_textSignal map:^id(NSString *userName) {
        return @([self isValidUsername:userName]);
    }];
    RACSignal *passwordSignal = [self.password.rac_textSignal map:^id(NSString *password) {
        return @([self isValidUsername:password]);
    }];
    RAC(self.password,backgroundColor) = [passwordSignal map:^id(NSNumber *passwordValid) {
        return [passwordValid boolValue]?[UIColor yellowColor]:[UIColor lightGrayColor];
    }];
    RAC(self.textField,backgroundColor) = [userNameSignal map:^id(NSNumber *passwordValid) {
        return [passwordValid boolValue]?[UIColor yellowColor]:[UIColor lightGrayColor];
    }];

userNameField & PasswordField : rac_textSignal ---> map ---> map ---> backgroundColor
RAC宏允许直接把信号的输出应用到对象的属性上。RAC宏有两个参数,第一个是需要设置属性值的对象,第二个是属性名。每次信号产生一个next事件,传递过来的值都会应用到该属性上。
你是否好奇为什么要创建两个分开的validPasswordSignal和validUsernameSignal呢,而不是每个输入框一个单独的管道呢?
答案原文:Are you wondering why you created separate validPasswordSignal and validUsernameSignal signals, as opposed to a single fluent pipeline for each text field? Patience dear reader, the method behind this madness will become clear shortly!

聚合信号

登录按钮只有当用户名和密码输入框的输入都有效时才工作。现在要把这里改成响应式的。
现在的代码中已经有可以产生用户名和密码输入框是否有效的信号了—— userNameSignal 和 passwordSignal了。现在需要做的就是聚合这两个信号来决定登录按钮是否可用。

    RACSignal *signUpActiveSignal = [RACSignal combineLatest:@[userNameSignal,passwordSignal] reduce:^id(NSNumber *userNameValid ,NSNumber *passwordValid){
        return @([userNameValid boolValue] && [passwordValid boolValue]);
    }];
  • 上面的代码使用combineLatest:reduce:方法把validUsernameSignal和validPasswordSignal产生的最新的值聚合在一起,并生成一个新的信号。每次这两个源信号的任何一个产生新值时,reduce block都会执行,block的返回值会发给下一个信号。
    注意:RACsignal的这个方法可以聚合任意数量的信号,reduce block的参数和每个源信号相关。ReactiveCocoa有一个工具类RACBlockTrampoline,它在内部处理reduce block的可变参数
    [signUpActiveSignal subscribeNext:^(NSNumber *signupActive) {
        self.loginButton.enabled = [signupActive boolValue];
    }];
  • 1.分割——信号可以有很多subscriber,也就是作为很多后续步骤的源。注意上图中那个用来表示用户名和密码有效性的布尔信号,它被分割成多个,用于不同的地方。
  • 2.聚合——多个信号可以聚合成一个新的信号,在上面的例子中,两个布尔信号聚合成了一个。实际上你可以聚合并产生任何类型的信号。这些改动的结果就是,代码中没有用来表示两个输入框有效状态的私有属性了。这就是用响应式编程的一个关键区别,你不需要使用实例变量来追踪瞬时状态。
响应式登录

当点击按钮时,rac_signalForControlEvents发送了一个next事件(事件的data是UIButton)。map操作创建并返回了登录信号,这意味着后续步骤都会收到一个RACSignal。这就是你在subscribeNext:这步看到的。

[[[self.loginButton rac_signalForControlEvents:UIControlEventTouchUpInside] map:^id(id value) {
      return [self signInSignal];
}]subscribeNext:^(id x) {
     NSLog(@"%@",x);
  }];
  • 上面的代码从按钮的UIControlEventTouchUpInside事件创建了一个信号,然后添加了一个订阅,在每次事件发生时都会输出log.编译运行,确保的确有log输出。按钮只在用户名和密码框输入有效时可用,所以在点击按钮前需要在两个文本框中输入一些内容。现在按钮有了点击事件的信号,下一步就是把它和登录流程连接起来。那么问题就来了,打开RWDummySignInService.h,看一下接口:这个service有3个参数,用户名、密码和一个完成回调block。这个block会在登录成功或失败时执行。你可以在按钮点击事件的subscribeNext: blcok里直接调用这个方法

上面问题的解决方法,有时候叫做信号中的信号,换句话说就是一个外部信号里面还有一个内部信号。你可以在外部信号的subscribeNext:block里订阅内部信号。不过这样嵌套太混乱啦,还好ReactiveCocoa已经解决了这个问题。

信号中的信号

解决的方法很简单,只需要把map操作改成flattenMap就可以了

    [[[self.loginButton rac_signalForControlEvents:UIControlEventTouchUpInside]flattenMap:^RACStream *(id value) {
        return [self signInSignal];
    }]subscribeNext:^(id x) {
        //        NSLog(@"%@",x);
    }];
 // 现在已经完成了大部分的内容,最后就是在subscribeNext步骤里添加登录成功后跳转的逻辑。把代码更新成下面的:
 [[[self.loginButton rac_signalForControlEvents:UIControlEventTouchUpInside]flattenMap:^RACStream *(id value) {
        return [self signInSignal];
    }]subscribeNext:^(id x) {
        BOOL success  = [x boolValue];
        if (success) {
            NSLog(@"登录成功逻辑处理");
        }else{
            NSLog(@"登录失败逻辑处理");
        }
    }];
添加附加操作(Adding side-effects)
  • doNext:是直接跟在按钮点击事件的后面。而且doNext: block并没有返回值。因为它是附加操作,并不改变事件本身。
    上面的doNext: block把按钮置为不可点击,隐藏登录失败提示。然后在subscribeNext: block里重新把按钮置为可点击,并根据登录结果来决定是否显示失败提示。
    [[[[self.loginButton rac_signalForControlEvents:UIControlEventTouchUpInside] 
      doNext:^(id x) {
         self.loginButton.enabled = NO;
    }]flattenMap:^RACStream *(id value) {
         return [self signInSignal];
    }]subscribeNext:^(id x) {
        BOOL success  = [x boolValue];
        if (success) {
            NSLog(@"登录成功逻辑处理");
        }else{
            NSLog(@"登录失败逻辑处理");
        }
    }];
}
/**
     上面的代码使用RACSignal的createSignal:方法来创建信号。方法的入参是一个block,这个block描述了这个信号。当这个信号有subscriber时,block里的代码就会执行。
     block的入参是一个subscriber实例,它遵循RACSubscriber协议,协议里有一些方法来产生事件,你可以发送任意数量的next事件,或者用error\complete事件来终止。本例中,信号发送了一个next事件来表示登录是否成功,随后是一个complete事件。
     这个block的返回值是一个RACDisposable对象,它允许你在一个订阅被取消时执行一些清理工作。当前的信号不需要执行清理操作,所以返回nil就可以了。
     可以看到,把一个异步API用信号封装是多简单!
 */
-(RACSignal *)signInSignal
{
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [APIManager loginWithUserName:self.textField.text andPassword:self.password.text responseMessage:^(BOOL responseObject) {
            [subscriber sendNext:@(responseObject)];
            [subscriber sendCompleted];
        }];
        return nil;
    }];
}

学习笔记第二部分

/*
     获取search text field 的text signal
     将其转换为颜色来标示输入是否有效
     然后在subscribeNext:block里将颜色应用到search text field的backgroundColor属性
*/
    [[self.searchText.rac_textSignal map:^id(NSString *text) {
        return [self isValidSearchText:text]?[UIColor yellowColor]:[UIColor lightGrayColor];
    }]subscribeNext:^(UIColor *color) {
        self.searchText.backgroundColor = color;
    }];
   //  如何取消订阅一个signal?在一个completed或者error事件之后,订阅会自动移除(马上就会讲到)。你还可以通过RACDisposable 手动移除订阅。
//   RACSignal的订阅方法都会返回一个RACDisposable实例,它能让你通过dispose方法手动移除订阅。下面是一个例子:
   RACSignal *baColorSignal = [self.searchText.rac_textSignal map:^id(NSString *text) {
       return [self isValidSearchText:text]?[UIColor yellowColor]:[UIColor lightGrayColor];
   }];
   RACDisposable *subscription = [baColorSignal subscribeNext:^(UIColor *color) {
       self.searchText.backgroundColor = color;
   }];
   [subscription dispose];
   // 注意: 综上所述,如果你创建了一个管道,但是没有订阅它,这个管道就不会执行,包括任何如doNext: block的附加操作。
   // subscribeNext:block中使用了self来获取text field的引用。block会捕获并持有其作用域内的值。因此,如果self和这个信号之间存在一个强引用的话,就会造成循环引用。循环引用是否会造成问题,取决于self对象的生命周期。如果self的生命周期是整个应用运行时,比如说本例,那也就无伤大雅。但是在更复杂一些的应用中,就不是这么回事了。
   // 为了避免潜在的循环引用,Apple的文档Working With Blocks(https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithBlocks/WorkingwithBlocks.html#//apple_ref/doc/uid/TP40011210-CH8-SW16)中建议获取一个self的弱引用。用本例来说就是下面这样的:
   __weak BController *weakSelf = self;
   RACSignal *baColorSignal = [self.searchText.rac_textSignal map:^id(NSString *text) {
       return [self isValidSearchText:text]?[UIColor yellowColor]:[UIColor lightGrayColor];
   }];
   
   RACDisposable *subscription = [baColorSignal subscribeNext:^(UIColor *color) {
       weakSelf.searchText.backgroundColor = color;
   }];
   [subscription dispose];
   // 在上面的代码中,__weak修饰符使bself成为了self的一个弱引用。注意现在subscribeNext:block中使用bself变量。不过这种写法看起来不是那么优雅。那么我们换种写法:
   @weakify(self)
   [[self.searchText.rac_textSignal map:^id(NSString *text) {
       return [self isValidSearchText:text]?[UIColor yellowColor]:[UIColor lightGrayColor];
   }]subscribeNext:^(UIColor *color) {
       @strongify(self)
       self.searchText.backgroundColor = color;
   }];
   
   上面的@weakify 和 @strongify 语句是在Extended Objective-C库中定义的宏,也被包括在ReactiveCocoa中。@weakify宏让你创建一个弱引用的影子对象(如果你需要多个弱引用,你可以传入多个变量),@strongify让你创建一个对之前传入@weakify对象的强引用。
   注意:如果你有兴趣了解 @weakify 和 @strongify 实际上做了什么,在Xcode中,选择Product -> Perform Action -> Preprocess “RWSearchForViewController”。这会对view controller 进行预处理,展开所有的宏,以便你能看到最终的输出。
网络层回调

还有一个机会来进一步接受我们函数反应型编程的理念,那就是我们的网络层 FRPPhotoImporter
,我们先来看看下载图片的方法:

+ (void)downloadThumbnailForPhotoModel:(FRPPhotoModel *)photoModel {
    [self download:photoModel.thumbnailURL withCompletion:^(NSData *data) {
        photoModel.thumbnailData = data;
    }];
}

+ (void)downloadFullsizedImageForPhotoModel:(FRPPhotoModel *)photoModel {
    [self download:photoModel.fullsizedURL withCompletion:^(NSData *data){
        photoModel.fullsizedData = data;
    }];
}

+ (void)download:(NSString *)urlString withCompletion:(void (^)(NSData *data))completion {
    NSAssert(urlString, @"URL must not be nil");

    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]];
    [NSURLConnection sendAsynchronousRequest:request
                                queue:[NSOperationQueue mainQueue]
                                completionHandler:
                                     ^(NSURLResponse *response, NSData *data, NSError *connectionError) {
                                            if(completion) {
                                                completion(data);
                                            }
                                     }];
}

Completion blocks?这是另外一个使用Signals的机会。更深入一点来说,我们可以使用NSURLConnection的ReactiveCocoa的扩展。下面我们来重写上面的方法:

+ (void)downloadThumbnailForPhotoModel:(FRPPhotoModel *)photoModel {
    RAC(photoModel, thumbnailData) = [self download:photoModel.thumbnailURL];
}

+ (void)downloadFullsizedImageForPhotoModel:(FRPPhotoModel *)photoModel {
    RAC(photoModel,fullsizedData) = [self download:photoModel.fullsizedURL];
}

+ (RACSignal *)download:(NSString *)urlString {
    NSAssert(urlString , @"URL must not be nil");

    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString: urlString]];

    return [[[NSURLConnection rac_sendAsynchronousRequest:request]
                map:^id (RACTuple *value) {
                    return [value second];
                }] deliverOn:[RACScheduler mainThreadScheduler]];
}

注意: RAC的操作,不在主线程中执行。这里 deliverOn:[RACScheduler mainThreadScheduler]] 的作用是回到主线程

推荐阅读更多精彩内容

  • 作为一个iOS开发者,你写的每一行代码几乎都是在相应某个事件,例如按钮的点击,收到网络消息,属性的变化(通过KVO...
    jiajia1118阅读 560评论 0 2
  • 作为一个iOS开发者,你写的每一行代码几乎都是在相应某个事件,例如按钮的点击,收到网络消息,属性的变化(通过KVO...
    不作不会死阅读 125评论 0 1
  • 前言 之前对RAC有了一个基本的认识,了解了它的作用,以及RAC的运行机制,我们知道只要是信号(RACSignal...
    大大盆子阅读 3,125评论 0 7
  • RAC使用测试Demo下载:github.com/FuWees/WPRACTestDemo 1.ReactiveC...
    FuWees阅读 3,324评论 3 11
  • 1.ReactiveCocoa常见操作方法介绍。 1.1 ReactiveCocoa操作须知 所有的信号(RACS...
    萌芽的冬天阅读 611评论 0 5