前言 本文是根据DeveloperLx 在斗鱼直播ReactiveCocoa
时整理的文章,同时加上本人的见解,若有错误希望指出。 感谢 DeveloperLx 为大家做出的贡献,虽然直播声音小了点,但满满的都是干货 ^v^
简介 神马是RAC?ReactiveCocoa(简称为RAC),是由Github开源的一个应用于iOS和OS开发的新框架,Cocoa是苹果整套框架的简称,因此很多苹果框架喜欢以Cocoa结尾。借用RayWenderlich 上面的话:
As an iOS developer, nearly every line of code you write is in reaction to some event; a button tap, a received network message, a property change (via Key Value Observing) or a change in user’s location via CoreLocation are all good examples. However, these events are all encoded in different ways; as actions, delegates, KVO, callbacks and others. ReactiveCocoa defines a standard interface for events, so they can be more easily chained, filtered and composed using a basic set of tools.
翻译过来就是:
作为一个iOS开发者,你写的每一行代码几乎都是在响应某个事件,例如按钮的点击,收到网络消息,属性的变化(通过KVO)或者用户位置的变化(通过CoreLocation)。但是这些事件都用不同的方式来处理,比如action、delegate、KVO、callback等。ReactiveCocoa为事件定义了一个标准接口,从而可以使用一些基本工具来更容易的连接、过滤和组合。
RAC是由 Mattt Thompson
大神开发的,很多开发者对其的评价是开启一个新Objective-C纪元
,可见对其评价有多高。 以下是RAC的Github主页:ReactiveCocoa 以及官方给出的用法链接
安装 ReactiveCocoa安装教程我就不说了,用pod安装即可。 本文的Demo可在文章最后下载,在阅读本文的时候,强烈推荐边看Demo边看博文。 项目中加入了ReactiveCocoa 和 DeveloperLx 大神的打印插件 LxDBAnything 。 同时加入了键盘相应的第三方IQKeyboardManager ,然而没有怎么用到。 以及Masonry ,使用方法可以看这篇文章,iOS - Masonry自动布局(Autolayout) 。
撸代码 第一部分 简单使用 文本框事件 原来我们在使用textFiled
的时候我们需要写到
1 [textField addTarget: self action: @selector (textChanged: ) forControlEvents: UIControlEventEditingChanged];
然后实现textChanged:
方法,在RAC中,对于文本框的监听,是非常简单的一件事情,看如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 UITextField * textField = ({ UITextField * textField = [[UITextField alloc]init] textField.backgroundColor = [UIColor cyanColor] textField }) [self.view addSubview:textField]; @weakify(self) [textField mas_makeConstraints:^(MASConstraintMaker *make) { @strongify(self) make.size.mas_equalTo(CGSizeMake(180 , 40 )) make.center.equalTo(self.view) }] [[textField rac_signalForControlEvents:UIControlEventEditingChanged] subscribeNext: ^(id x) { LxDBAnyVar(x) }] [textField.rac_textSignal subscribeNext:^(NSString *x) { LxDBAnyVar(x) }]
打印结果:
1 2 3 4 5 6 7 8 9 📍__31-[ViewController textFiledTest]_block_invoke_2 + 215 🎈 x = 12 📍__31-[ViewController textFiledTest]_block_invoke241 + 211 🎈 x = <UITextField: 0 x7fe810c51a90; frame = (97.5 313.5 ; 180 40 ); text = '123 '; clipsToBounds = YES; opaque = NO; gestureRecognizers = <NSArray: 0 x7fe810f58fb0>; layer = <CALayer: 0 x7fe810c51600>> 📍__31-[ViewController textFiledTest]_block_invoke_2 + 215 🎈 x = 123 📍__31-[ViewController textFiledTest]_block_invoke241 + 211 🎈 x = <UITextField: 0 x7fe810c51a90; frame = (97.5 313.5 ; 180 40 ); text = '1231 '; clipsToBounds = YES; opaque = NO; gestureRecognizers = <NSArray: 0 x7fe810f58fb0>; layer = <CALayer: 0 x7fe810c51600>> 📍__31-[ViewController textFiledTest]_block_invoke_2 + 215 🎈 x = 1231 📍__31-[ViewController textFiledTest]_block_invoke241 + 211 🎈 x = <UITextField: 0 x7fe810c51a90; frame = (97.5 313.5 ; 180 40 ); text = '12312 '; clipsToBounds = YES; opaque = NO; gestureRecognizers = <NSArray: 0 x7fe810f58fb0>; layer = <CALayer: 0 x7fe810c51600>> 📍__31-[ViewController textFiledTest]_block_invoke_2 + 215 🎈 x = 12312 📍__31-[ViewController textFiledTest]_block_invoke241 + 211 🎈 x = <UITextField: 0 x7fe810c51a90; frame = (97.5 313.5 ; 180 40 ); text = '123123 '; clipsToBounds = YES; opaque = NO; gestureRecognizers = <NSArray: 0 x7fe810f58fb0>; layer = <CALayer: 0 x7fe810c51600>> 📍__31-[ViewController textFiledTest]_block_invoke_2 + 215 🎈 x = 123123
我们很容易的监听到textFiled
中发生的变化,其中x的类型默认为id类型, 我们已知它的类型的时候我们可以将其改变,就像上面代码,将id改成了NSString类型。
手势 1 2 3 4 5 6 7 self .view.userInteractionEnabled = YES ; UITapGestureRecognizer * tap = [[UITapGestureRecognizer alloc]init]; [[tap rac_gestureSignal] subscribeNext:^(UITapGestureRecognizer * tap) { LxDBAnyVar(tap); }]; [self .view addGestureRecognizer:tap];
为了方便,我们直接添加到self.view上,点击屏幕,得到打印结果:
1 📍__29-[ViewController gestureTest]_block_invoke + 184 🎈 tap = <UITapGestureRecognizer: 0 x7fa2e3e1f9f0; state = Ended; view = <UIView 0 x7fa2e3e20b70>; target= <(action=sendNext:, target=<RACPassthroughSubscriber 0 x7fa2e3c064f0>)>>
通知 1 2 3 4 [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil] subscribeNext:^(NSNotification * notification) { LxDBAnyVar(notification ) }]
我们建立了一个通知,叫做进入后台, 当程序进入后台的时候通知相应,当我们用RAC写通知的时候,我们有一个好处,就是不用removeObserver通知,因为RAC通知的监听者师RAC自己,它会帮你管理释放方法。可以看方法实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 - (RACSignal *)rac_addObserverForName:(NSString *)notificationName object :(id)object { @unsafeify (object ) ; return [[RACSignal createSignal:^(id<RACSubscriber > subscriber) { @strongify (object ) ; id observer = [self addObserverForName:notificationName object :object queue :nil usingBlock:^(NSNotification *note) { [subscriber sendNext:note]; }]; return [RACDisposable disposableWithBlock:^{ [self removeObserver:observer]; }]; }] setNameWithFormat:@"-rac_addObserverForName: %@ object: <%@: %p>" , notificationName, [object class ], object ] ; }
定时器 1 2 3 4 5 6 7 8 9 10 11 [[RACScheduler mainThreadScheduler]afterDelay: 2 schedule: ^{ LxPrintAnything(rac); }]; [[RACSignal interval: 1 onScheduler: [RACScheduler mainThreadScheduler]]subscribeNext: ^(NSDate * date) { LxDBAnyVar(date); }];
这是定时器最常用的两种写法,第一种方法,延迟时间去做某件事,更改afterDelay
的属性。 第二种方法,每间隔多长时间做一件事,更改interval
属性。
代理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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]; [[alertView rac_buttonClickedSignal]subscribeNext: ^(id x) { LxDBAnyVar(x); }];
用RAC去写代理的时候,会有局限,只能取代没有返回值的代理方法,什么是没有返回值的代理呢?比如说tableView的代理方法:
1 2 3 4 - (CGFloat) tableView:(UITableView *) tableView heightForRowAtIndexPath:(NSIndexPath *) indexPath - (void) tableView:(UITableView *) tableView didSelectRowAtIndexPath:(NSIndexPath *) indexPath
这两个方法一个返回的是CGFloat
,一个是void
,RAC只能取代void的代理。
KVO 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 UIScrollView * scrollView = [[UIScrollView alloc]init] scrollView.delegate = (id<UIScrollViewDelegate>)self[self.view addSubview:scrollView]; UIView * scrollViewContentView = [[UIView alloc]init] scrollViewContentView.backgroundColor = [UIColor yellowColor][scrollView addSubview:scrollViewContentView]; @weakify(self) [scrollView mas_makeConstraints:^(MASConstraintMaker *make) { @strongify(self) make.edges.equalTo(self.view).insets(UIEdgeInsetsMake(80, 80 , 80 , 80 )) }] [scrollViewContentView mas_makeConstraints:^(MASConstraintMaker *make) { @strongify(self) make.edges.equalTo(scrollView); make.size.mas_equalTo(CGSizeMake(CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame))) }] [RACObserve(scrollView, contentOffset) subscribeNext:^(id x) { LxDBAnyVar(x) }]
用RAC写KVO的好处就是方法简单,keypath有代码提示。
第二部分 进阶 信号 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 - (RACSignal *)loginSignal { return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { RACDisposable * schedulerDisposable = [[RACScheduler mainThreadScheduler]afterDelay:2 schedule:^{ if (arc4random()%10 > 1 ) { [subscriber sendNext:@"Login response" ] [subscriber sendCompleted] } else { [subscriber sendError:[NSError errorWithDomain:@"LOGIN_ERROR_DOMAIN" code:444 userInfo:@{}]] } }] return [RACDisposable disposableWithBlock:^{ [schedulerDisposable dispose]; }] }] }
RAC的核心就是RACSignal,也就是信号,我们可以直接创建信号createSignal
,并发送它sendNext
,当信号完成后我们同时用dispose
方法销毁它。发送信号,我们同时也要订阅信号,订阅信号代码如下:1 2 3 4 5 6 7 8 9 10 [signal subscribeNext:^(id x) { LxDBAnyVar(x ) } error:^(NSError *error) { LxDBAnyVar(error ) } completed: LxPrintAnything(completed); } ]
在信号发送的时候, 错误的时候,以及完成的时候,我们都可以得到相应。
信号的处理 map (映射) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 UITextField * textField = ({ UITextField * textField = [[UITextField alloc]init]; textField .backgroundColor = [UIColor cyanColor]; textField ; }); [self.view addSubview:textField ]; @weakify(self); [textField mas_makeConstraints:^(MASConstraintMaker *make) { @strongify(self); make.size .mas_equalTo(CGSizeMake(180 , 40 )); make.center.equalTo(self.view); }]; [[textField .rac_textSignal map:^id(NSString *text ) { LxDBAnyVar(text ); return @(text .length); }] subscribeNext:^(id x) { LxDBAnyVar(x); }];
map这个函数,在这里不是地图的意思,代表映射。map能做的事情就是把监听的rac_textSignal
所返回的值,替换成别的就像上面代码中的text的长度。
filter 为了方便演示,我就不再赋值创建textField
的代码了,请到Demo中查看1 2 3 4 5 6 7 8 9 10 11 12 13 [[[textField.rac_textSignal map:(NSString *text) { LxDBAnyVar(text ) return @(text.length ) }]filter:(NSNumber *value) { return value.integerValue > 3 }] subscribeNext:^(id x) { LxDBAnyVar(x ) }]
filter是个BOOL值,它代表的是一个条件,当这个条件发生的时候才会作出相应,比如上面代码中,当长度大于3的时候,才会打印x的值。
delay 1 2 3 4 5 6 7 8 9 10 11 RACSignal * signal = [[RACSignal createSignal: ^RACDisposable *(id<RACSubscriber> subscriber) { [subscriber sendNext: @"rac" ]; [subscriber sendCompleted]; return nil; }]delay: 2 ]; LxPrintAnything(start); [signal subscribeNext: ^(id x) { LxDBAnyVar(x); }];
delay
的作用就是延迟,或者说等待,如上,等待2秒之后打印了x。
startWith 1 2 3 4 5 6 7 8 9 10 11 12 RACSignal * signal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber > subscriber) { // [subscriber sendNext:@"123" ];//startWith:@"123" 等同于这句话 也就是第一个发送,主要是位置 [subscriber sendNext:@"rac" ]; [subscriber sendCompleted]; return nil; }]startWith:@"123" ]; LxPrintAnything (start); //创建订阅者 [signal subscribeNext:^(id x) { LxDBAnyVar (x); }];
startWith
也就是最开始的意思,看以上代码 startWith:@"123"
等同于[subscriber sendNext:@"123"]
也就是第一个发送,主要是位置.
timeOut 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 [[[RACSignal createSignal: *(id<RACSubscriber> subscriber) { [[RACScheduler mainThreadScheduler]afterDelay:3 schedule: [subscriber sendNext:@"rac"]; [subscriber sendCompleted]; } ] return nil }] timeout:2 onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(id x) { LxDBAnyVar(x ) } error:^(NSError *error) { LxDBAnyVar(error ) } completed: LxPrintAnything(completed); } ]
上面代码的意思就是,我设置了超时限制为(timeout)2秒钟,但是我代码延迟3秒钟发送,超时了,所以这条信息发生错误,会走error的方法。 这种情况可以用在封装http client
中,当然你可能遇到别的需求,也需要它。
take – skip 1 2 3 4 5 6 7 8 9 10 11 12 RACSignal * signal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { [subscriber sendNext:@"rac1" ] [subscriber sendNext:@"rac2" ] [subscriber sendNext:@"rac3" ] [subscriber sendNext:@"rac4" ] [subscriber sendCompleted] return nil }]take:2 ] [signal subscribeNext:^(id x) { LxDBAnyVar(x) }]
比如说我们发送了很多次请求
take表示我们只取前两次 skip表示跳过前两次 takeLast表示倒数的前两次 takeUntil这个值比较特殊,他后面的参数是个信号,它的意思是,当takeUntil发送这个信号的时候,上面的发送信号就会停止发送。
接下来是几个block回调方法
takeWhileBlock BOOL值,意思是当返回YES的时候,订阅者才能收到信号 skipWhileBlock BOOL值,意思是当返回YES的时候,订阅者就会跳过信号,NO的时候才接受 skipUntilBlock BOOL值,意思是 返回NO的时候,不会收到消息, 直到返回YES的时候才开始收消息。
即时搜索优化 (throttle,distinctUntilChanged,ignore) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 UITextField * textField = [[UITextField alloc]init]; textField.backgroundColor = [UIColor cyanColor]; [self .view addSubview:textField]; @weakify(self ); [textField mas_makeConstraints:^(MASConstraintMaker *make) { @strongify(self ); make.size.mas_equalTo(CGSizeMake (180 , 40 )); make.center.equalTo(self .view); }]; [[[[[[textField.rac_textSignal throttle:0.3 ] distinctUntilChanged] ignore:@"" ] map:^id (id value) { return [RACSignal createSignal:^RACDisposable *(id <RACSubscriber> subscriber) { [subscriber sendNext:value]; [subscriber sendCompleted]; return [RACDisposable disposableWithBlock:^{ }]; }]; }]switchToLatest] subscribeNext:^(id x) { LxDBAnyVar(x); }];
以上代码,是用textField模拟一个即时搜索优化的功能,其中参数如下:
throttle 后面是个时间 表示rac_textSignal发送消息,0.3秒内没有再次发送就会相应,若是0.3内又发送消息了,便会在新的信息处重新计时 distinctUntilChanged 表示两个消息相同的时候,只会发送一个请求 ignore 表示如果消息和ignore后面的消息相同,则会忽略掉这条消息,不让其发送
这样做,是不是给服务器减小了很多的压力,更是节省了我们大量的代码。 其中我们用map建立了一个新的信号,我们知道textField的改变是一个信号, map就是在这个信号上,又加了一个信号,即signal of signals
。 订阅者所打印的消息x则是,map发出的信号。我们可以再map中发送新的信号,以及取消信号disposable
. 当我们用map发送信号的时候,我们则需要使用 switchToLatest
这个参数来获取最后一个信号,也就是我们最后所打印的x,就是map最后发错的这个信号。
repeat 1 2 3 4 5 6 7 8 9 10 11 12 13 [[[[[RACSignal createSignal: *(id<RACSubscriber> subscriber) { [subscriber sendNext:@"rac" ] [subscriber sendCompleted] return nil }]delay:1]repeat]take:3] subscribeNext:^(id x) { LxDBAnyVar(x ) } completed: LxPrintAnything(completed); } ]
repeat,顾名思义,就是重复发送这条消息,当我们在后面添加了delay和take的时候,意思就是每隔1秒发送一次这条消息,发送3次后停止。
merge – concat – zipWith 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 RACSignal * signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ LxPrintAnything(a) [subscriber sendNext:@"a" ] [subscriber sendCompleted] }) return nil }] RACSignal * signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ LxPrintAnything(b); [subscriber sendNext:@"b" ] [subscriber sendCompleted] }) return nil }] [[RACSignal merge:@[signalA, signalB]]subscribeNext:^(id x) { LxDBAnyVar(x) }]
我们创建了两个请求,A和B,用GCD的方法A延迟两秒钟,B延迟了3秒钟,我们用merge
方法合并了A和B,打印结果为1 2 3 4 📍__23-[ViewController merge ]_block_invoke_2 + 66 🎈 a 📍__23-[ViewController merge ]_block_invoke29 + 87 🎈 x = a 📍__23-[ViewController merge ]_block_invoke_215 + 77 🎈 b 📍__23-[ViewController merge ]_block_invoke29 + 87 🎈 x = b
也就是A和B不管谁发送都会打印x,简单的说就是A和B的打印方法用的是同一个。他们之间关系是独立的,如果A发送失败,B依然会执行。
当我们用concat
方法链接A和B之后,意思就是当A执行完了之后才会执行B,他们之间是依赖的关系,如果A发送失败,B也不会执行。
请注意合并(merge)和链接(concat)的区别。
zipWith
,当用zipWith
链接A和B的时候,只有在A.B每隔都至少发送过一次消息的时候才会执行zipWith的方法,它的返回值是一个集合,也就是数组,同时包含了A和B的打印结果。zipWith
的写法等同于 :
1 2 3 4 [[RACSignal combineLatestWith:@[signalA, signalB]subscribeNext:^(id x) { LxDBAnyVar(x ) }]
亦或者
1 2 3 4 [[RACSignal combineLatest:@[signalA, signalB]]subscribeNext:^(id x) { LxDBAnyVar(x ) }]
但是使用combineLatest
,可以再后面添加更多的信号.
RAC(<#TARGET, …#>) 宏 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 //button setBackgroundColor:forState: UIButton * button = [UIButton buttonWithType:UIButtonTypeCustom]; [self.view addSubview:button]; [button mas_makeConstraints:^(MASConstraintMaker *make) { make.size.mas_equalTo(CGSizeMake(180 , 40 )) make.center.equalTo(self.view); }] RAC(button, backgroundColor) = [RACObserve(button, selected) map :^UIColor *(NSNumber * selected) { return [selected boolValue] ? [UIColor redColor] : [UIColor greenColor] }] [[button rac_signalForControlEvents:UIControlEventTouchUpInside]subscribeNext:^(UIButton * btn) { btn.selected = !btn.selected; }]
比如Btn的设置背景颜色的属性,OC中并没有button setBackgroundColor:forState:
这种方法,我们不能直接设置其选中后的颜色。在RAC中,则可以很简单的改变BTN的背景颜色。不得不说RAC的简单和强大。
做一个秒表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 UILabel * label = ({ UILabel * label = [[UILabel alloc]init] label.backgroundColor = [UIColor cyanColor] label }) [self.view addSubview:label]; @weakify(self) [label mas_makeConstraints:^(MASConstraintMaker *make) { @strongify(self) make.size.mas_equalTo(CGSizeMake(240 , 40 )) make.center.equalTo(self.view) }] RAC(label, text) = [[RACSignal interval:1 onScheduler:[RACScheduler mainThreadScheduler]] map:^NSString *(NSDate * date) { return date.description }]
只有这么多代码,我们便可以完美的做一个秒表,是否很cool?
结束 当我们大量使用RAC写代码的时候,会把一个个事件封装成一个个信号,通过触发信号,订阅这个信号来返回各种信息。RAC使我们的代码耦合性根底,聚合性更高。
若有不懂得地方可以留言,若有写错的地方,请及时与我联系,可以留言或者Email等。
文本所用的Demo,下载地址 戳这里 .