ReactiveCocoa 是 github 开源的一个函数式、响应式编程框架,是在 iOS 平台上对 FRP 的实现。

RAC 解决的主要问题

iOS 开发中消息传递和回调机制一直很复杂,RAC 使用 Signal 来代替 KVO、Notification、Delegate 和 Target-Action 等传递消息,解决对象之间状态与状态依赖过多的问题,RAC 通常和 MVVM 结合在一起,在很多地方被用作 iOS 项目中 MVVM 架构的实践方式。

常用几招

target-action类:

TextField

需求:实时监听 textField 输入的字符串并打印。
传统 target-action 方式:

//注册 selector
[textField addTarget:self action:@selector(textChanged:) forControlEvents:UIControlEventEditingChanged];
//实现 selector
- (void)textChanged:(UITextField *)textField
{
      LxDBAnyVar(textField);
}

这里用到一个很方便的用于打印对象的工具LxDBAnyVar
用 RAC 的方式:

[[textField rac_signalForControlEvents:UIControlEventEditingChanged]
   subscribeNext:^(id x) {
    LxDBAnyVar(x);
}];

事实上对于所有 UIControl 子类的对象的事件监听都可以用这种方式。比如 UIButton 的TouchUpInside事件。
更简洁版本:

[textField.rac_textSignal subscribeNext:^(NSString *x) {     
    LxDBAnyVar(x);
}];

手势

需求:为 UIView 添加点击事件。

self.view.userInteractionEnabled = YES;

UITapGestureRecognizer * tap = [[UITapGestureRecognizer alloc]init];  
    [[tap rac_gestureSignal] subscribeNext:^(UITapGestureRecognizer * tap) {
        LxDBAnyVar(tap);
    }];

[self.view addGestureRecognizer:tap];

通知

需求:监听 app 进入后台的通知。

[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil] subscribeNext:^(NSNotification * notification) {
        LxDBAnyVar(notification);
}];

注意:使用 RAC 监听通知不需要removeObserver。因为监听者是 RAC 内部持有的,RAC 会管理通知什么时候释放。

定时器 NSTimer

需求1. 延迟某个时间后再做某件事。
更改afterDelay属性:

[[RACScheduler mainThreadScheduler]afterDelay:2 schedule:^{
        LxPrintAnything(rac);
}];

需求2. 每间隔多长时间做一件事。
更改interval属性:

[[RACSignal interval:1 onScheduler:[RACScheduler mainThreadScheduler]]subscribeNext:^(NSDate * date) {
        LxDBAnyVar(date);
}];

代理

需求:监听点击了 AlertView 的哪一个按钮。

UIAlertView * alertView = [[UIAlertView alloc]initWithTitle:@"RAC" message:@"ReactiveCocoa" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Ensure", nil];  
[[self rac_signalForSelector:@selector(alertView:clickedButtonAtIndex:) fromProtocol:@protocol(UIAlertViewDelegate)] subscribeNext:^(RACTuple * tuple) {
        LxDBAnyVar(tuple);
        LxDBAnyVar(tuple.first);
        LxDBAnyVar(tuple.second);
        LxDBAnyVar(tuple.third);
}];
[alertView show];

tuple是 RAC 自己定义的集合类,特点是一个对象含有多个对象。此处对应 alertView 中的按钮。
更简单方式:

[[alertView rac_buttonClickedSignal]subscribeNext:^(id x) {
        LxDBAnyVar(x);
}];

RAC 取代代理有局限:只能是没有返回值(void)的代理方法。

KVO

需求:监听 scrollView 滑动时 contentOffset 的变化。

[RACObserve(scrollView, contentOffset) subscribeNext:^(id x) {
        LxDBAnyVar(x);
}];

进阶

RAC 常见类

RACSignal (核心)

过程:创建信号 -> 激活信号 -> 废弃信号。
RAC的核心就是RACSignal,我们可以直接创建信号createSignal,并发送sendNext,当信号完成后用dispose销毁。

// 1.创建信号
RACSignal *siganl = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

        // block调用时刻:每当有订阅者订阅信号,就会调用block。

        // 2.发送信号:注意signal本身不具备发送信号的能力,而是交给内部一个订阅者去发出。
        [subscriber sendNext:@1];

        // 如果不再发送数据,最好发送信号完成,内部会自动调用[RACDisposable disposable]取消订阅信号。
        [subscriber sendCompleted];

        return [RACDisposable disposableWithBlock:^{

            // block调用时刻:当信号发送完成或者发送错误,就会自动执行这个block,取消订阅信号。
            // 执行完Block后,当前信号就不在被订阅了。
            NSLog(@"信号被销毁");

        }];
    }];

    // 3.订阅信号,才会激活信号.
    [siganl subscribeNext:^(id x) {
        // block调用时刻:每当有信号发出数据,就会调用block.
        NSLog(@"接收到数据:%@",x);
    }error:^(NSError *error) {

     NSLog(error);
   }completed:^{

     NSLog(@"completed");
 }];

RACCommand

处理事件的类。如监听按钮点击,网络请求,可以把事件如何处理,事件中的数据如何传递等包装到这个类中。

// 1.创建命令
RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {


    NSLog(@"执行命令");

    // 创建空信号,必须返回信号
    //        return [RACSignal empty];

    // 2.创建信号,用来传递数据
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

        [subscriber sendNext:@"请求数据"];

        // 注意:数据传递完,最好调用sendCompleted,这时命令才执行完毕。
        [subscriber sendCompleted];

        return nil;
    }];

}];

// 强引用命令,不要被销毁,否则接收不到数据
_conmmand = command;

// 3.订阅RACCommand中的信号
[command.executionSignals subscribeNext:^(id x) {

    [x subscribeNext:^(id x) {

        NSLog(@"%@",x);
    }];

}];

// RAC高级用法
// switchToLatest:用于signal of signals,获取signal of signals发出的最新信号,也就是可以直接拿到RACCommand中的信号
[command.executionSignals.switchToLatest subscribeNext:^(id x) {

    NSLog(@"%@",x);
}];

// 4.监听命令是否执行完毕,默认会来一次,可以直接跳过,skip表示跳过第一次信号。
[[command.executing skip:1] subscribeNext:^(id x) {

    if ([x boolValue] == YES) {
        // 正在执行
        NSLog(@"正在执行");

    }else{
        // 执行完成
        NSLog(@"执行完成");
    }

}];
// 5.执行命令
[self.conmmand execute:@1];

RAC 常见宏

RAC(TARGET, [KEYPATH, [NIL_VALUE]]=…

用于给某个对象的某个属性绑定信号。

// 只要文本框文字改变,就会修改label的文字
RAC(self.label,text) = _textField.rac_textSignal;

RACObserve(TARGET, [KEYPATH])

用于监听某个对象的某个属性,返回信号。

[RACObserve(scrollView, contentOffset) subscribeNext:^(id x) {
        NSLog(x);
}];

信号的处理

映射 flattenMap,Map

用于把源信号内容映射成新的内容。
Map 使用:

  1. 传入一个block,类型是返回对象,参数是value。
  2. value就是源信号的内容,直接拿到源信号的内容做处理。
  3. 把处理好的内容,直接返回就好了,不用包装成信号,返回的值,就是映射的值。
[[_textField.rac_textSignal map:^id(id value) {
        // 当源信号发出,就会调用这个block,修改源信号的内容
        // 返回值:就是处理完源信号的内容。
        return [NSString stringWithFormat:@"输出:%@",value];
    }] subscribeNext:^(id x) {

        NSLog(@"%@",x);
}];

combineLatest

多个异步请求都完成后,再做某件事。
将多个信号合并起来,并且拿到各个信号的最新的值,必须每个合并的 signal 至少都有过一次 sendNext,才会触发合并的信号。

RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

   [subscriber sendNext:@1];

   return nil;
}];

RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

   [subscriber sendNext:@2];

   return nil;
}];

// 把两个信号组合成一个信号,跟zip一样,没什么区别
RACSignal *combineSignal = [signalA combineLatestWith:signalB];

[combineSignal subscribeNext:^(id x) {

   NSLog(@"%@",x);
}];

当组合信号被订阅,内部会自动订阅signalA、signalB,必须两个信号都发出内容,才会被触发。

reduce

用于信号发出的内容是元组,把信号发出元组的值聚合成一个值。
常见用法:先组合再聚合。

combineLatest:(id<NSFastEnumeration>)signals reduce:(id (^)())reduceBlock  
RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

   [subscriber sendNext:@1];

   return nil;
}];

RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

   [subscriber sendNext:@2];

   return nil;
}];

// reduce中的block简介:
// reduceblcok中的参数,有多少信号组合,reduceblcok就有多少参数,每个参数就是之前信号发出的内容
// reduceblcok的返回值:聚合信号之后的内容。
RACSignal *reduceSignal = [RACSignal combineLatest:@[signalA,signalB] reduce:^id(NSNumber *num1 ,NSNumber *num2){

  return [NSString stringWithFormat:@"%@ %@",num1,num2];

}];

[reduceSignal subscribeNext:^(id x) {

   NSLog(@"%@",x);
}];

实战:用 RAC 实现登录模块


如图是常见的一个登录需求,登录按钮在信息填写完整且符合规定前不可用,我们尝试用 RAC+MVVM 实现。项目结构如图:

model 层

@interface User : NSObject

@property NSString  * user_id;
@property NSString  * access_token;
@property NSString  *phone_num;
@property NSString  *password;
@property BOOL      isLogin;

@end

view-model 层


MVVM 相比 MVC 多了 view-model 层,将原先 controller 层的大部分业务逻辑抽离出来,本案例主要涉及两个:

  1. 判断是否允许登录
  2. 执行登录动作

因此在 LoginViewModel 中设置两个信号。

@interface LoginViewModel : NSObject

@property(nonatomic,strong) User *user;

//是否允许登录的信号
@property(nonatomic,strong,readonly) RACSignal *enableLoginSignal;
//执行登录操作的信号
@property(nonatomic,strong,readonly) RACCommand *loginCommand;

@end

判断是否允许登录

//    监听账号的属性值改变,把它们聚合为一个信号。
_enableLoginSignal = [RACSignal combineLatest:@[RACObserve(self.user, phone_num),RACObserve(self.user, password)] reduce:^id(NSString *phone_num,NSString *password){

    NSPredicate *phoneNum_prdicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",@"^1[3|4|5|7|8][0-9]\\d{8}$"];
    NSPredicate *pwd_predicate     = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",@"^.{6,}$"];
    return @([phoneNum_prdicate evaluateWithObject:phone_num] && [pwd_predicate evaluateWithObject:password]);

}];

看得出来相比以前用target-action监听 textField 变化,再用if...else判断两个值是否都存在且合法,现在利用 RAC conbine + reduce处理信号,只需单个函数即可实现,代码清爽很多。

执行登录动作

_loginCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) {

    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

        [User loginWithParameters:_user.mj_keyValues SuccessBlock:^(id returnValue) {

            User* newUser = [User mj_objectWithKeyValues:returnValue];
            _user.user_id      = newUser.user_id;
            _user.access_token = newUser.access_token;
            _user.isLogin      = YES;

            [subscriber sendNext:@"success"];
            [subscriber sendCompleted];

        } FailureBlock:^(NSError *error) {

            [subscriber sendNext:[NSString stringWithFormat:@"登录失败!%@",error]];
            [subscriber sendCompleted];

        }];

        return nil;

    }];
}];

注意raccommand返回的需是一个信号,登录操作就在这里面,登录之后发送信号。
loginCommand还可以监听登录状态。

[[_loginCommand.executing skip:1]subscribeNext:^(id x) {

    if ([x isEqualToNumber:@(YES)]) {
        [Config showProgressHUDwithStatus:@"登录中..."];
    } else {
        [Config dismissHUD];
    }
}];
@end

controller 层

@interface LoginVC : UIViewController

@property (strong, nonatomic) IBOutlet UITextField    *accountTextField;
@property (strong, nonatomic) IBOutlet UITextField    *pwdTextField;
@property (strong, nonatomic) IBOutlet UIButton       *loginBtn;
@property (strong,nonatomic ) LoginViewModel          *loginViewModel;

@end

视图模型绑定

// 给模型的属性绑定信号
// 账号文本框一改变,就给 User 属性赋值    
RAC(self.loginViewModel.user,phone_num) = _accountTextField.rac_textSignal;
RAC(self.loginViewModel.user,password)  = _pwdTextField.rac_textSignal;

// 为登录按钮的"enable"属性绑定信号
RAC(self.loginBtn,enabled)              = self.loginViewModel.enableLoginSignal;

监听登录动作

// 监听登录按钮的点击,执行loginCommand
[[_loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside]subscribeNext:^(id x) {

// 执行登录
    [self.loginViewModel.loginCommand execute:nil];

}];

监听登录结果

订阅loginCommand返回的信号,如果返回 success 就存储登录信息。

[self.loginViewModel.loginCommand.executionSignals.switchToLatest subscribeNext:^(NSString* x) {

    if ([x isEqualToString:@"success"]) {

//  存储用户信息及登录状态,此处不能使用 realm 作数据库,realm 不允许在 observer 中 addObject。

        NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];

        [defaults setValue:_loginViewModel.user.phone_num forKey:@"phone_num"];
        [defaults setValue:_loginViewModel.user.access_token forKey:@"access_token"];
        [defaults setValue:[NSNumber numberWithInteger:_loginViewModel.user.user_id] forKey:@"user_id"];
        [defaults setValue:[NSNumber numberWithBool:_loginViewModel.user.isLogin] forKey:@"isLogin"];
        [defaults synchronize];

        [self performSegueWithIdentifier:@"toTabBarController" sender:nil];

    }
}];

上述登录代码可参考这里