ReactiveCocoa是Github团队开发的第三方函数式响应式编程框架,在目前市面上的很多iOS App都大量使用了这个框架。以下我简称这个框架为RAC.我下面会通过几篇博客来和大家一起学习这个强大的框架。该博客的案例代码已经上传至 https://github.com/chenyufeng1991/ReactiveCocoaDemo 。当然最好的学习方式是去阅读RAC的源码,Github上面RAC的官网地址 https://github.com/ReactiveCocoa/ReactiveCocoa 。在官网中,包含了源码,代码示例,文档。在本篇博客中,我主要是对官方文档进行翻译,并加入自己的理解与实现。这里实现的语言为OC。
【1】ReactiveCocoa(RAC)介绍
RAC是iOS的一个函数式响应式编程框架,而不是使用可变的变量去修改和替换原有的值。RAC提供了信号(RACSignal类)来监听当前和未来的值。通过信号的链接、组合和响应,可以让我们的代码持续的观察和更新值。我用一句话说就是:响应数据的变化。
举个例子,我们可以绑定一个TextField输入框,只要绑定的值有改变,我们可以不添加任何额外的代码,就可以更新该输入框。工作原理类似于KVO,但是使用block块来替代重写“observeValueForKeyPath:ofObject:change:context”这个方法。信号也代表了异步操作,可以简化网络请求等异步代码。RAC的一个最主要优势就是提供了信号,统一处理了iOS中的异步行为,包括delegate,block回调,target-action机制,Notification和KVO。如下的例子:
// When self.username changes,logs the new name to the console. // // RACObserve(self,username) creates a new RACSignal that sends the current // value of self.username,then the new value whenever it changes. // -subscribeNext: will execute the block whenever the signal sends a value. [RACObserve(self,username) subscribeNext:^(NSString *newName) { NSLog(@"%@",newName); }];
当self.username的值改变时,log中就会输出新的值。RACObserve创建了一个新的RACSignal对象,可以发送最新的值到self.username,因此值就会随时改变。当信号signal发送新的值时,-subscribeNext就会执行block块中的代码。
但是和KVO不一样,信号可以被链起来并操作,如下代码所示:
// Only logs names that starts with "j". // // -filter returns a new RACSignal that only sends a new value when its block // returns YES. [[RACObserve(self,username) filter:^(NSString *newName) { return [newName hasPrefix:@"j"]; }] subscribeNext:^(NSString *newName) { NSLog(@"%@",newName); }];
上面的log中只会输出包含前缀为j的字符串。-filter会返回新的RACSignal对象,可以根据block返回新的值。
信号同样可以用来得到状态,可以很方便的给属性一个信号和操作。如下代码所示:
// Creates a one-way binding so that self.createEnabled will be // true whenever self.password and self.passwordConfirmation // are equal. // // RAC() is a macro that makes the binding look nicer. // // +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. RAC(self,createEnabled) = [RACSignal combineLatest:@[ RACObserve(self,password),RACObserve(self,passwordConfirmation) ] reduce:^(NSString *password,NSString *passwordConfirm) { return @([passwordConfirm isEqualToString:password]); }];
以上代码创建了一种新的数据绑定的方式,当self.password和self.passwordConfirmation相等的时候会返回true。RAC()是宏,可以让数据绑定看起来更加良好。+combineLatest:reduce: 是信号的数组,只要任意一个信号中的值有改变,就会用最新的值去执行block中的代码,然后返回新的RACSignal对象,用来发送新值。
信号可以随时创建在任何值的流上,不同于KVO。举个例子,信号可以代表按钮点击:
// Logs a message whenever the button is pressed. // // RACCommand creates signals to represent UI actions. Each signal can // represent a button press,for example,and have additional work associated // with it. // // -rac_command is an addition to NSButton. The button will send itself on that // command whenever it's pressed. self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) { NSLog(@"button was pressed!"); return [RACSignal empty]; }];
每当按钮点击的时候就会输出日志。RACCommand创建了一个信号表示UI事件。每一个信号可以表示一个按钮点击,并可以执行相关的操作。同样的,RACCommand也可以进行异步网络操作,如下:
// Hooks up a "Log in" button to log in over the network. // // This block will be run whenever the login command is executed,starting // the login process. self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) { // The hypothetical -logIn method returns a signal that sends a value when // the network request finishes. return [client logIn]; }]; // -executionSignals returns a signal that includes the signals returned from // the above block,one for each time the command is executed. [self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) { // Log a message whenever we log in successfully. [loginSignal subscribeCompleted:^{ NSLog(@"Logged in successfully!"); }]; }]; // Executes the login command when the button is pressed. self.loginButton.rac_command = self.loginCommand;
上面的代码用来连接登录按钮和网络操作。当开始登录的时候将会去执行第一个block,在block中假设的logIn方法将会在网络请求结束的时候返回一个信号。-executeSignals将会返回信号,包括了上面第一个block中的信号。
使用信号也可以用来表示定时器,其他的UI事件,或者随着事件变化的操作。使用信号可以让复杂的异步操作通过链式和传递信号变得更加简单。当一组操作完成后信号就可以被触发,如下:
// Performs 2 network operations and logs a message to the console when they are // both completed. // // +merge: takes an array of signals and returns a new RACSignal that passes // through the values of all of the signals and completes when all of the // signals complete. // // -subscribeCompleted: will execute the block when the signal completes. [[RACSignal merge:@[ [client fetchUserRepos],[client fetchOrgRepos] ]] subscribeCompleted:^{ NSLog(@"They're both done!"); }];
当client的两个网路请求都完成后,控制台就会打印出信息。+merge:获得信号数组,当数组中的信号都完成后,返回RACSignal对象。在异步操作中,信号可以被链式然后按序列执行,而不用使用嵌套的block回调。如下所示:
// Logs in the user,then loads any cached messages,then fetches the remaining // messages from the server. After that's all done,logs a message to the // console. // // The hypothetical -logInUser methods returns a signal that completes after // logging in. // // -flattenMap: will execute its block whenever the signal sends a value,and // returns a new RACSignal that merges all of the signals returned from the block // into a single signal. [[[[client logInUser] flattenMap:^(User *user) { // Return a signal that loads cached messages for the user. return [client loadCachedMessagesForUser:user]; }] flattenMap:^(NSArray *messages) { // Return a signal that fetches any remaining messages. return [client fetchMessagesAfterMessage:messages.lastObject]; }] subscribeNext:^(NSArray *newMessages) { NSLog(@"New messages: %@",newMessages); } completed:^{ NSLog(@"Fetched all messages."); }];
用户登录,先加载缓存数据,然后从远程服务器抓取数据,以上操作完成后,打印log。 假设的-logInUser方法当登录完成后会返回信号。 -flattenMap:方法当信号发送一个值的时候就会去执行block,并返回一个新的RACSignal对象,该对象会合并上面所有的信号为一个单一信号。RAC可以使绑定异步操作的结果更加简单:
// Creates a one-way binding so that self.imageView.image will be set as the user's // avatar as soon as it's downloaded. // // The hypothetical -fetchUserWithUsername: method returns a signal which sends // the user. // // -deliverOn: creates new signals that will do their work on other queues. In // this example,it's used to move work to a background queue and then back to the main thread. // // -map: calls its block with each user that's fetched and returns a new // RACSignal that sends values returned from the block. RAC(self.imageView,image) = [[[[client fetchUserWithUsername:@"joshaber"] deliverOn:[RACScheduler scheduler]] map:^(User *user) { // Download the avatar (this is done on a background queue). return [[NSImage alloc] initWithContentsOfURL:user.avatarURL]; }] // Now the assignment will be done on the main thread. deliverOn:RACScheduler.mainThreadScheduler];
创建了一个绑定,当用户头像下载完成后,self.imageView.image就会被立即设置。假设的-fetchUserWithUsername:会发送一个信号。 -deliverOn:创建一个信号可以让任务在其他队列中去执行。在这个例子中,是用来让任务在后台队列执行然后切换到主线程。
上面简单描述了RAC可以做的一些事情,但是没有说明为什么RAC如此强大。如果想看更多的示例代码,可以查看C-41和GroceryList这两个项目,这两个项目都是用RAC来写的。
【何时使用RAC】
当第一次看到RAC的时候,感觉非常的抽象,理解起来也非常的困难,以致于很难在具体的问题中使用。这里有一些具体在哪些情况下使用RAC的建议:
1.处理异步任务或者事件驱动数据源的时候
大多数Cocoa的程序都是关注于响应用户的事件。但是处理此类事件的代码会很快变得很复杂,因为有大量的回调和状态变量。这种模式从表面上看起来都很不一样,像UI回调,网络响应,KVO,其实他们有很多都是共通的。RACSignal统一了这些不同的API,并让我们使用相同的方式来调用。下面代码:
static void *ObservationContext = &ObservationContext; - (void)viewDidLoad { [super viewDidLoad]; [LoginManager.sharedManager addObserver:self forKeyPath:@"loggingIn" options:NSKeyValueObservingOptionInitial context:&ObservationContext]; [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(loggedOut:) name:UserDidlogoutNotification object:LoginManager.sharedManager]; [self.usernameTextField addTarget:self action:@selector(updateLogInButton) forControlEvents:UIControlEventEditingChanged]; [self.passwordTextField addTarget:self action:@selector(updateLogInButton) forControlEvents:UIControlEventEditingChanged]; [self.logInButton addTarget:self action:@selector(logInPressed:) forControlEvents:UIControlEventTouchUpInside]; } - (void)dealloc { [LoginManager.sharedManager removeObserver:self forKeyPath:@"loggingIn" context:ObservationContext]; [NSNotificationCenter.defaultCenter removeObserver:self]; } - (void)updateLogInButton { BOOL textFieldsNonEmpty = self.usernameTextField.text.length > 0 && self.passwordTextField.text.length > 0; BOOL readyToLogIn = !LoginManager.sharedManager.isLoggingIn && !self.loggedIn; self.logInButton.enabled = textFieldsNonEmpty && readyToLogIn; } - (IBAction)logInPressed:(UIButton *)sender { [[LoginManager sharedManager] logInWithUsername:self.usernameTextField.text password:self.passwordTextField.text success:^{ self.loggedIn = YES; } failure:^(NSError *error) { [self presentError:error]; }]; } - (void)loggedOut:(NSNotification *)notification { self.loggedIn = NO; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == ObservationContext) { [self updateLogInButton]; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } }
我们也可以把上述代码改写成RAC形式:
- (void)viewDidLoad { [super viewDidLoad]; @weakify(self); RAC(self.logInButton,enabled) = [RACSignal combineLatest:@[ self.usernameTextField.rac_textSignal,self.passwordTextField.rac_textSignal,RACObserve(LoginManager.sharedManager,loggingIn),loggedIn) ] reduce:^(NSString *username,NSString *password,NSNumber *loggingIn,NSNumber *loggedIn) { return @(username.length > 0 && password.length > 0 && !loggingIn.boolValue && !loggedIn.boolValue); }]; [[self.logInButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *sender) { @strongify(self); RACSignal *loginSignal = [LoginManager.sharedManager logInWithUsername:self.usernameTextField.text password:self.passwordTextField.text]; [loginSignal subscribeError:^(NSError *error) { @strongify(self); [self presentError:error]; } completed:^{ @strongify(self); self.loggedIn = YES; }]; }]; RAC(self,loggedIn) = [[NSNotificationCenter.defaultCenter rac_addObserverForName:UserDidlogoutNotification object:nil] mapReplace:@NO]; }
依赖在网络请求中很常见,比如下一个请求之前要先去完成前一个请求。比如:
[client logInWithSuccess:^{ [client loadCachedMessagesWithSuccess:^(NSArray *messages) { [client fetchMessagesAfterMessage:messages.lastObject success:^(NSArray *nextMessages) { NSLog(@"Fetched all messages."); } failure:^(NSError *error) { [self presentError:error]; }]; } failure:^(NSError *error) { [self presentError:error]; }]; } failure:^(NSError *error) { [self presentError:error]; }];而RAC可以让这种模式变得简单,改造如下:
[[[[client logIn] then:^{ return [client loadCachedMessages]; }] flattenMap:^(NSArray *messages) { return [client fetchMessagesAfterMessage:messages.lastObject]; }] subscribeError:^(NSError *error) { [self presentError:error]; } completed:^{ NSLog(@"Fetched all messages."); }];
3.并行独立任务
在并行任务中处理独立的数据集,并把它们组合成最后的结果,这样的操作往往会涉及大量的同步操作,我们常用的代码如下:
__block NSArray *databa@R_404_401@bjects; __block NSArray *fileContents; NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init]; NSBlockOperation *databa@R_404_401@peration = [NSBlockOperation blockOperationWithBlock:^{ databa@R_404_401@bjects = [databaseClient fetchObjectsMatchingPredicate:predicate]; }]; NSBlockOperation *filesOperation = [NSBlockOperation blockOperationWithBlock:^{ NSMutableArray *filesInProgress = [NSMutableArray array]; for (NSString *path in files) { [filesInProgress addObject:[NSData dataWithContentsOfFile:path]]; } fileContents = [filesInProgress copy]; }]; NSBlockOperation *finishOperation = [NSBlockOperation blockOperationWithBlock:^{ [self finishProcessingDataba@R_404_401@bjects:databa@R_404_401@bjects fileContents:fileContents]; NSLog(@"Done processing"); }]; [finishOperation addDependency:databa@R_404_401@peration]; [finishOperation addDependency:filesOperation]; [backgroundQueue addOperation:databa@R_404_401@peration]; [backgroundQueue addOperation:filesOperation]; [backgroundQueue addOperation:finishOperation];
上面的代码可以优化为简单的组合信号,RAC后的代码如下:
RACSignal *databaseSignal = [[databaseClient fetchObjectsMatchingPredicate:predicate] subscribeOn:[RACScheduler scheduler]]; RACSignal *fileSignal = [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id<RACSubscriber> subscriber) { NSMutableArray *filesInProgress = [NSMutableArray array]; for (NSString *path in files) { [filesInProgress addObject:[NSData dataWithContentsOfFile:path]]; } [subscriber sendNext:[filesInProgress copy]]; [subscriber sendCompleted]; }]; [[RACSignal combineLatest:@[ databaseSignal,fileSignal ] reduce:^ id (NSArray *databa@R_404_401@bjects,NSArray *fileContents) { [self finishProcessingDataba@R_404_401@bjects:databa@R_404_401@bjects fileContents:fileContents]; return nil; }] subscribeCompleted:^{ NSLog(@"Done processing"); }];
4.简化集合操作
高阶函数如map,filter,fold/reduce是没有在Foundation框架中的,会导致循环的代码如下:
NSMutableArray *results = [NSMutableArray array]; for (NSString *str in strings) { if (str.length < 2) { continue; } NSString *newString = [str stringByAppendingString:@"foobar"]; [results addObject:newString]; }而使用RACSequence可以对Cocoa中的集合操作进行统一处理,改造代码如下:
RACSequence *results = [[strings.rac_sequence filter:^ BOOL (NSString *str) { return str.length >= 2; }] map:^(NSString *str) { return [str stringByAppendingString:@"foobar"]; }];
【系统要求】
RAC要求OS X10.8+,iOS 8.0+.
【导入RAC】
个人推荐使用CocoaPods来导入RAC。可以查看C-41和GroceryList这两个项目,这两个项目里面已经包含了RAC.
【独立开发】
如果独立的开发RAC而不是把它集成到一个项目中,你应该要去打开ReactiveCocoa.xcworkspace 而不是.xcodeproj.
【更多资料】
RAC是基于.NET的Reactive Extensions(Rx),很多Rx种的原理都可以应用到RAC中,下面是一些Rx的资源:
Reactive Extensions MSDN entry
Reactive Extensions for .NET Introduction
Programming Reactive Extensions and LINQ
RAC和Rx都是一种函数式响应式编程(Functional Reactive Programming),下面是关于FRP的资源:
What is Functions Reactive Programming - Stack Overflow
Specification for a Functional Reactive Language - Stack Overflow
Principles of Reactive Programming on Coursera
本文大部分翻译自 :https://github.com/ReactiveCocoa/ReactiveCocoa/blob/v2.5/README.md