ReactiveCocoa 入门与登录实战
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 使用:
- 传入一个block,类型是返回对象,参数是value。
- value就是源信号的内容,直接拿到源信号的内容做处理。
- 把处理好的内容,直接返回就好了,不用包装成信号,返回的值,就是映射的值。
[[_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 层的大部分业务逻辑抽离出来,本案例主要涉及两个:
- 判断是否允许登录
- 执行登录动作
因此在 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];
}
}];
上述登录代码可参考这里