原文地址:http://www.teehanlax.com/blog/getting-started-with-reactivecocoa/
在先前的文章中,介绍一了ReactiveCocoa概念,ReactiveCocoa是Objective-C中用于声明式编程的类库。接下来在这里会介绍一些ReactiveCocoa中的模式,讨论一些最佳实践,并指出一些常见的陷阱。ReactiveCocoa的学习需要时间,让我们慢慢来。
在ReactiveCocoa中有三种基本的模式:责任链、分割和组合模式(chaining, splitting, and combining)。之前的文章时稍微介绍了责任链和组合模式,接下来的会更深入。
回顾一下:在ReactiveCocoa中的核心是signal:(信号),它表示不断变化的状态。当我们使用chain、split和combine时,实际上我们就是在操作这些signal。
Chaining是在ReactiveCocoa最常用的模式:将一个已有的signal转换为一个新的signal。常用的操作是创建一个新的signal,再对它使用filter:、map:或startWith:等方法。例:
RAC(self.textField.text) = [[[RACSignal interval:1] startWith:[NSDate date]] map:^id(NSDate *value) {
NSDateComponents *dateComponents = [[NSCalendar currentCalendar] components:NSMinuteCalendarUnit | NSSecondCalendarUnit fromDate:value];
return [NSString stringWithFormat:@"%d:%02d", dateComponents.minute, dateComponents.second];
}];
在这个例子中,我们将textFiled的text属性绑定为三个串连的signals的结果。首先,我们创建一个间隔信号,这个信号每隔一秒钟就发送当前时间。间隔信号在没有启动的时候是不会有值的,所以我们使用startWith:启动起来。最后,使用map:将signal的NSDate值转换为一个NSString字符串,这个字符串将会被赋值到textField的text属性上。
Chaining是最常用的操作,而且它通常不使用局部变量,而是像上面那样串连起来操作。下面的代码与上面的代码是等同的。
RACSignal *intervalSignal = [RACSignal interval:1];
RACSignal *startedIntervalSignal = [intervalSignal startWith:[NSDate date]];
RACSignal *mappedIntervalSignal = [startedIntervalSignal map:^id(NSDate *value) {
NSDateComponents *dateComponents = [[NSCalendar currentCalendar] components:NSMinuteCalendarUnit | NSSecondCalendarUnit fromDate:value];
return [NSString stringWithFormat:@"%d:%02d", dateComponents.minute, dateComponents.second];
}];
RAC(self.textField.text) = mappedIntervalSignal;
Splitting与chaining比较类似,也是将signal转换为其它的sginal,不同之处在于,Splitting会重复使用中间的signals。Splitting看起来要复杂些,其实也就是一个signals使用多次罢了。例:
RACSignal *dateComponentsSignal = [[[RACSignal interval:1] startWith:[NSDate date]] map:^id(NSDate *value) {
NSDateComponents *dateComponents = [[NSCalendar currentCalendar] components:NSMinuteCalendarUnit | NSSecondCalendarUnit fromDate:value];
return dateComponents;
}];
RAC(self.minuteTextField.text) = [dateComponentsSignal map:^id(NSDateComponents *dateComponents) {
return [NSString stringWithFormat:@"%d", dateComponents.minute];
}];
RAC(self.secondTextField.text) = [dateComponentsSignal map:^id(NSDateComponents *dateComponents) {
return [NSString stringWithFormat:@"%d", dateComponents.second];
}];
在上面这个例子中,创建了一个串联signal,即局部变量:dateComponentsSignal。接着再用dateComponentsSignal创建两个新的signal,并将它们分别与两个textfield的text属性进行绑定。
第三种常用模式是combining。combining就是将几个signal结合起来创建出一个新的signal。比如“登录”按钮,只有在“用户名”与“密码”输入框中的文本长度都超过6时才能被点击,否则处于不可用的状态。那么我们可以为“登录”按钮的enabled状态创建一个signal,这个signal则是由“用户名”与“密码”框它们两个自己的signal组合起来:
RAC(self.submitButton.enabled) = [RACSignal combineLatest:@[self.usernameField.rac_textSignal, self.passwordField.rac_textSignal] reduce:^id(NSString *userName, NSString *password) {
return @(userName.length >= 6 && password.length >= 6);
}];
在这里,我们将“登录”按钮的enable状态绑定到使用combineLatest:reduce:方法创建的signal上。这个方法的第二个参数是一个block,这个block的参数是combineLatest中的参数的最新值的组合。我们将两个文本框的text signal一起传到combineLatest,在reduce的block中,该block也就会接收到两个NSString的参数,这个block的工作就是将两个参数值组合起来生成一个值,然后返回。该方法的说明:
// +combineLatest:reduce: takes an array of signals, executes the block with the
// latest value from each signal whenever any of them changes, and returns a new
// RACSignal that sends the return value of that block as values.
Combining常用于两种情况:
1、需要同时满足多种条件。
2、在多个signal中进行选择。
重点在于这种线性逻辑(linear flow of logic)的思维,如何将这些signals进行串联、分割或组合。看看这些基本操作能让你对这些模式更加熟悉。
我们已经介绍了ReactiveCocoa模式的基本知识,接下来看看最佳实践。
ReactiveCocoa通过移除状态使我们写程序更容易。然而,即使是在一个“完成反应(completely reactive)”式的应用中,我们还是得写些非ReactiveCocoa的代码,比如像table view的delegate方法。RACSubjects则充当了非reactive和 reactive代码的桥梁。
RACSubject是能够手动发送新值的signal。比如,gesture recognizers并不是ReactiveCocoa的一部分——这时我们可以用两个RACSubject属性:一个用于接收gesture recognizer:事件,标识这个recognizer是否正在处理;另一个用来记录它当前的位置。
self.gestureRecognizerIsRunningSubject = [RACSubject subject];
self.gestureRecognizerValueSubject = [RACSubject subject];
RAC(self.someView.frame) = [self.gestureRecognizerValueSubject map:^id(NSValue *value) {
CGPoint location = [value CGPointValue];
CGFloat size = 100.0f;
return [NSValue valueWithCGRect:CGRectMake(location.x - size/2.0f, location.y - size/2.0f, size, size)];
}];
我们将一个view放到gesture recognizer的最后位置的中心。
发送这些subjects事件非常简单,只要简单实现一个gesture recognizer方法即可(注:该方法可放到代理方法-gestureRecognizer:shouldReceiveTouch:中调用):
-(void)gestureRecognizerReceivedTouch:(UIPanGestureRecognizer *)recognizer {
if (recognizer.state == UIGestureRecognizerStateBegan) {
[self.gestureRecognizerIsRunningSubject sendNext:@(YES)];
}
else if (recognizer.state == UIGestureRecognizerStateChanged) {
[self.gestureRecognizerValueSubject sendNext:[NSValue valueWithCGPoint:[recognizer locationInView:self.view]]];
}
else if (recognizer.state == UIGestureRecognizerStateEnded) {
[self.gestureRecognizerIsRunningSubject sendNext:@(NO)];
}
}
虽然RACSubjects是非reactive代码与ReactiveCocoa代码的桥梁,但过分滥用也是有风险的。当我们能够通过chaining signals完成任务的话,就不要依赖于RACSubjects的值。
ReactiveCocoa的设计就是让我们的程序尽可能地减少各种状态,这种减少状态的逻辑使得我们要少用执行副作用(perform side-effects)。比如基于上面的代码,我们希望在table view的手势事件完成时,闪烁一个滑动条,让用户知道已经滑到哪里了。我们可以调用subscription:
[[self.gestureRecognizerIsRunningSubject filter:^BOOL(NSNumber *gestureRecognizerIsRunning) {
return !(gestureRecognizerIsRunning.boolValue);
}] subscribeNext:^(id x) {
[self.tableView flashScrollIndicators];
}];
这里我们过滤掉手势正在运行的事件,只在滑动完成时subcribe。这时,在subscribeNext: bloc中,我们执行了副作用。
虽然subscriptions很有用,执行副作用也是必要的,但要小心过度使用。它们就是可变的变量、状态,这些正是ReactiveCocoa所避免的。在能够通过绑定属性映射signals完成任务的时候,就不要使用RACSubjects。
任何一种新的东西,对于新手来说总会有些陷阱。比如下面的代码,当我们从一个属性创建一个新的signal,在someString的值改变之前,其实是什么也不会发生的。
RAC(self.label.text) = RACAble(self.someString);
如果想要立即发送someSting当前的值,可以用RACAbleWithStart。这个“starts”的signal会将someString的当前的值也与之绑定。
RAC(self.label.text) = RACAbleWithStart(self.someString);
与之类似,当使用interval:安排一个周期定时器时,这个定时器不会立即启用走到这个传来第一个interval,有点像用NSTimer。还记得第一个例子不?我们将text field的text值与当前时间绑定,我们是手动使用startWith:并传当前时间来开启的。如果我们不这么做的话,text field在第一个间隔时间的前一秒是空的。
关于interval:还有一个重点需要注意的时,这个方法的将它的结果传递到高优化级的调度中(类似于GCD队列)。也就是说之前的代码实际上有一个微秒的BUG的:不能直接执行更新UI的代码。可以将interval: signal的结果传递到主线程调度中结果这个问题:
RAC(self.textField.text) = [[[[RACSignal interval:1] startWith:[NSDate date]] map:^id(NSDate *value) {
NSDateComponents *dateComponents = [[NSCalendar currentCalendar] components:(NSMinuteCalendarUnit|NSSecondCalendarUnit) fromDate:value];
return [NSString stringWithFormat:@"%d:%02d", dateComponents.minute, dateComponents.second];
}] deliverOn:[RACScheduler mainThreadScheduler]];
这样就好点了。
这篇文章介绍了一些常用模式,最佳实践还有一些陷阱。希望能帮助你利用ReactiveCocoa构建声明式应用程序。