本文介绍了函数响应式编程思想,通过信号来记录值的变化,同时信号可以被叠加、分割或合并,来处理复杂逻辑,从而实现函数响应式编程。同时本文还介绍了在IOS平台下基于函数响应式编程思想的第三方开源库ReactiveCocoa及其实践。除了介绍ReactiveCocoa的具体编程实践,还介绍了MVVM设计模式,它不同于传统的MVC设计模式,具有低偶和、可重用性、独立开发、可测试性等优点。
关键词 函数响应式编程 ReactiveCocoa MVVM
1 ReactiveCocoa
ReactiveCocoa是Github开源的一款cocoa FRP框架,它具有函数响应式编程的特性。Functional Reactive Programming(以下简称FRP)是一种响应变化的编程。FRP提供了一种信号机制来实现这样的效果,通过信号来记录值的变化。信号可以被叠加、分割或合并。通过对信号的组合,就不需要去监听某个值或事件。这在重交互的应用里是非常有用的。如图1,以注册为例:
提交按钮的状态要跟输入框的状态绑定,比如必选的输入框没有填完时,提交按钮是灰色的,也就是不可点;如果提交按钮不可点,那么文字变成灰色,否则变成蓝色;如果正在提交,那么输入框的文字颜色变成灰色,且不可点,否则变成默认色且可点;如果注册成功就在状态栏显示成功信息,否则显示错误信息,等等。
使用FRP主要有两个好处:直观和灵活。直观的代码容易编写、阅读和维护,灵活的特性便于应对变态的需求。
2几个常见的概念
2.1 Signal and Subscriber
Signal and Subscriber是RAC最核心的内容,这里我想用插头和插座来描述,插座是Signal,插头是Subscriber。想象某个遥远的星球,他们的电像某种物质一样被集中存储,且很珍贵。插座负责去获取电,插头负责使用电,而且一个插座可以插任意数量的插头。当一个插座(Signal)没有插头(Subscriber)时什么也不干,也就是处于冷(Cold)的状态,只有插了插头时才会去获取,这个时候就处于热(Hot)的状态。
Signal获取到数据后,会调用Subscriber的sendNext, sendComplete, sendError方法来传送数据给Subscriber,Subscriber自然也有方法来获取传过来的数据,如:[signal subscribeNext:error:completed]。这样只要没有sendComplete和sendError,新的值就会通过sendNext源源不断地传送过来,举个简单的例子:
[RACObserve(self,username)subscribeNext:^(NSString*newName) NSLog(@"newName:%@",newName); }];
RACObserve使用了KVO来监听property的变化,只要username被自己或外部改变,block就会被执行。但不是所有的property都可以被RACObserve,该property必须支持KVO,比如NSURLCache的currentDiskUsage就不能被RACObserve。
Signal是很灵活的,它可以被修改(map),过滤(filter),叠加(combine),串联(chain),这有助于应对更加复杂的情况,比如:
RAC(self.logInButton,enabled)=[RACSignal combineLatest:@[ self.usernameTextField.rac_textSignal, self.passwordTextField.rac_textSignal, RACObserve(LoginManager.sharedManager,loggingIn), RACObserve(self,loggedIn) ]reduce:^(NSString*username,NSString*password,NSNumber*loggingIn,NSNumber*loggedIn){ return@(username.length>0&&password.length>0&&!loggingIn.boolValue&&!loggedIn.boolValue); }];
这段代码看起来有点复杂,下面详细说一下,首先是左边的RAC(...),它的作用是将self.logInButton.enabled属性与右边的signal的sendNext值绑定。也就是如果右边的reduce的返回值为NO,那么enabled就为NO。右边的combineLatest是获取这4个signal的next值。其中可以看到self.usernameTextField.rac_textSignal,rac_textSignal是RAC为UITextField添加的category,只要usernameTextField的值有变化,这个值就会被返回(sendNext)。combineLatest需要每个signal至少都有过一次sendNext。reduce的作用是根据接收到的值,再返回一个新的值,这里是@(YES)和@(NO),必须是object。
上面这段代码用到了Signal的组合,想象一下,如果是传统的方式,写起来还是挺复杂的,而且随着功能的增加,调整起来会更加麻烦。
2.2 冷信号(Cold)和热信号(Hot)
上面提到过这两个概念,冷信号默认什么也不干,比如下面这段代码:
RACSignal*signal=[RACSignalcreateSignal:^RACDisposable*(id subscriber){ NSLog(@"triggered"); [subscribersendNext:@"foobar"]; [subscribersendCompleted]; returnnil; }];
这里创建了一个Signal,但因为没有被subscribe,所以什么也不会发生。加了下面这段代码后,signal就处于Hot的状态了,block里的代码就会被执行。
[signalsubscribeCompleted:^{ NSLog(@"subscription%u",subscriptions); }];
如果这时又有一个新的subscriber了,signal的block还会被执行吗?这就牵扯到了另一个概念:Side Effect。
2.3 Side Effect
还是上面那段代码,如果有多个subscriber,那么signal就会又一次被触发,控制台里会输出两次triggered。这或许是想要的,或许不是。如果要避免这种情况的发生,可以使用 replay 方法,它的作用是保证signal只被触发一次,然后把sendNext的value存起来,下次再有新的subscriber时,直接发送缓存的数据。
3 Cocoa Categories
3.1 UIView Categories
rac_textSignal是加在UITextField上的(UITextField+RACSignalSupport.h),其它常用的UIView也都有添加相应的category,比如UIAlertView,就不需要再用Delegate了。
UIAlertView*alertView=[[UIAlertViewalloc]initWithTitle:@""message:@"Alert"delegate:nilcancelButtonTitle:@"YES"otherButtonTitles:@"NO",nil]; [[alertViewrac_buttonClickedSignal]subscribeNext:^(NSNumber*indexNumber){ if([indexNumberintValue]==1){ NSLog(@"youtouchedNO"); }else{ NSLog(@"youtouchedYES"); } }]; [alertViewshow];
有了这些Category,大部分的Delegate都可以使用RAC来做。或许你会想,可不可以subscribe NSMutableArray.rac_sequence.signal,这样每次有新的object或旧的object被移除时都能知道,UITableViewController就可以根据dataSource的变化,来reloadData。但很可惜这样不行,因为RAC是基于KVO的,而NSMutableArray并不会在调用addObject或removeObject时发送通知,所以不可行。不过可以使用NSArray作为UITableView的dataSource,只要dataSource有变动就换成新的Array,这样就可以了。
3.2 Data Structure Categories
常用的数据结构,如NSArray, NSDictionary也都有添加相应的category,比如NSArray添加了rac_sequence,可以将NSArray转换为RACSequence,顺便说一下RACSequence, RACSequence是一组immutable且有序的values,不过这些values是运行时计算的,所以对性能提升有一定的帮助。RACSequence提供了一些方法,如array转换为NSArray,any:检查是否有Value符合要求,all:检查是不是所有的value都符合要求,这里的符合要求的,block返回YES,不符合要求的就返回NO。
3.3 NotificationCenter Category
默认情况下NSNotificationCenter使用Target-Action方式来处理Notification,这样就需要另外定义一个方法,这就涉及到编程领域的两大难题之一:起名字。有了RAC,就有Signal,有了Signal就可以subscribe,于是NotificationCenter就可以这么来处理,还不用担心移除observer的问题。
[[[NSNotificationCenterdefaultCenter]rac_addObserverForName:@"MyNotification"object:nil]subscribeNext:^(NSNotification*notification){ NSLog(@"NotificationReceived"); }];
3.4 NSObject+RACSelectorSignal.h
category有rac_signalForSelector:和rac_signalForSelector:fromProtocol: 这两个方法。先来看前一个,它的意思是当某个selector被调用时,再执行一段指定的代码,相当于hook。比如点击某个按钮后,记个日志。后者表示该selector实现了某个协议,所以可以用它来实现Delegate。
4 MVVM
RAC带来的变化还不仅仅是这些,它还带来了架构层面的变化,MVVM构架如图2所示。
MVVM跟MVC最大的区别是多了个ViewModel,它直接与View绑定,而且对View一无所知。拿做菜打比方的话,ViewModel就是调料,它不关心做的到底是什么菜。这跟Model很像,它可以扮演Model的职责,但其实它是Model的中介,这样当Model的API有变化,或者由本地存储变为远程API调用时,ViewModel的public API可以保持不变。
使用ViewModel的好处是,可以让Controller更加简单和轻便,而且ViewModel相对独立,也更加方便测试和重用。在MVVM体系中,Controller可以被看成View,它的主要工作是处理布局、动画、接收系统事件、展示UI。
MVVM还有一个很重要的概念是 data binding,view的呈现需要data,这个data就是由ViewModel提供的,将view的data与ViewModel的data绑定后,将来双方的数据只要一方有变化,另一方就能收到。
5 结语
本文介绍了ios平台下的一款开源第三方库ReactiveCocoa, 同时还对具体的使用场景也做了详细介绍。另外,本文中还介绍了MVVM设计模式以及其优点。为IOS开发人员提供了一种新的思路。