ReactiveCocoa,最受欢迎的iOS函数响应式编程库(2.5版),没有之一!
简介
- 项目主页: ReactiveCocoa
- 实例下载: https://github.com/ios122/ios122
- 简评: 最受欢迎,最有价值的iOS响应式编程库,没有之一!iOS MVVM模式的完美搭档,更多关于MVVM与ReactiveCocoa的讨论,参考这篇文章: 【长篇高能】ReactiveCocoa 和 MVVM 入门
- 注意: ReactiveCocoa 最新3.0版本,使用Swift重写,最低支持iOS8.0,与国内大多数公司实际现状(一般要求最低兼容iOS7.0)不符;故此处选择兼容性版本更低的 2.5 版本来进行对译与解读.
系统要求
- iOS 7.0 + (ReactiveCocoa 2.5 版本)
安装
推荐使用 CocoaPods 安装:
platform :ios,'7.0'
pod "ReactiveCocoa" @H_404_32@# RAC,一个支持响应式编程的库.
入门
ReactiveCocoa 灵感来源于 函数响应式编程. ReactiveCocoa通常简称为RAC.RAC中,不再使用变量,而是使用信号(以 RACSignal
为代表)来捕捉现在和未来的数据或视图的值.
通过对信号的链接,组合与响应,软件就可以声明式的方式书写;这样就不再需要频繁地去监测和更新数据或视图的值了.
RAC 主要特性之一就是提供了一种单一又统一的方式来处理各种异步操作–包括代理方法,block回调,target-action机制,通知和KVO等.
这是一个简单的例子:
@H_404_32@// 当self.username变化时,在控制台打印新的名字.
@H_404_32@//
@H_404_32@// RACObserve(self,username) 创建一个新的 RACSignal 信号对象,它将会发送self.username当前的值,和以后 self.username 发生变化时 的所有新值.
@H_404_32@// -subscribeNext: 无论signal信号对象何时发送消息,此block回调都将会被执行.
[RACObserve(self,username) subscribeNext:^(NSString *newName) {
NSLog(@"%@",newName);
}];
但是和KVO不同的是,signals信号对象支持链式操作:
@H_404_32@// 只打印以"j"开头的名字.
@H_404_32@//
@H_404_32@// -filter: 当其bock方法返回YES时,才会返回一个新的RACSignal 信号对象;即如果其block方法返回NO,信号不再继续往下传播.
[[RACObserve(self,username)
filter:^(NSString *newName) {
return [newName hasPrefix:@"j"];
}]
subscribeNext:^(NSString *newName) {
NSLog(@"%@",newName);
}];
Signals信号也可以用于派生属性(即那些由其他属性的值决定的属性,如Person可能有一个属性为 age年龄 和一个属性 isYong是否年轻,isYong 是由 age 属性的值推断而来,由age本身的值决定).不再需要来监测某个属性的值,然后来对应更新其他受此属性的新值影响的属性的值.RAC 可以支持以signales信号和操作的方式来表达派生属性.
@H_404_32@// 创建一个单向绑定,self.password和self.passwordConfirmation 相等
@H_404_32@// 时,self.createEnabled 会自动变为true.
@H_404_32@//
@H_404_32@// RAC() 是一个宏,使绑定看起来更NICE.
@H_404_32@//
@H_404_32@// +combineLatest:reduce: 使用一个 signals 信号的数组;
@H_404_32@// 在任意signal变化时,使用他们的最后一次的值来执行block;
@H_404_32@// 并返回一个新的 RACSignal信号对象来将block的值用作属性的新值来发送;
@H_404_32@// 简单说,类似于重写createEnabled 属性的 getter 方法.
RAC(self,createEnabled) = [RACSignal
combineLatest:@[ RACObserve(self,password),RACObserve(self,passwordConfirmation) ]
reduce:^(NSString *password,NSString *passwordConfirm) {
return @([passwordConfirm isEqualToString:password]);
}];
@H_404_32@// 使用时,是不需要考虑属性是否是派生属性以及以何种方式绑定的:
[RACObserve(self,createEnabled) subscribeNext: ^(NSNumber * enbable){
NSLog(@"%@",enbable);
}];
Signals信号可以基于任何随时间变化的数据流创建,不仅仅是KVO.例如说,他们可以用来表示一个按钮的点击事件:
@H_404_32@// 任意时间点击按钮,都会打印一条消息.
@H_404_32@//
@H_404_32@// RACCommand 创建代表UI事件的signals信号.例如,单个信号都可以代表一个按钮被点击,
@H_404_32@// 然后会有一些额外的操作与处理.
@H_404_32@//
@H_404_32@// -rac_command 是NSButton的一个扩展.按钮被点击时,会将会把自身发送给rac_command self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
NSLog(@"button was pressed!");
return [RACSignal empty];
}];
或者异步网络请求:
@H_404_32@// 监听"登陆"按钮,并记录网络请求成功的消息.
@H_404_32@// 这个block会在来任意开始登陆步骤,执行登陆命令时调用.
self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) {
@H_404_32@// 这是一个假想中的 -logIn 方法,返回一个 signal信号对象,这个对象在网络对象完成时发送 值.
@H_404_32@// 可以使用 -filter 方法来保证当且仅当网络请求完成时,才返回一个 signal 对象.
return [client logIn];
}];
@H_404_32@// -executionSignals 返回一个signal对象,这个signal对象就是每次执行命令时通过上面的block返回的那个signal对象.
[self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) {
@H_404_32@// 打印信息,不论何时我们登陆成功.
[loginSignal subscribeCompleted:^{
NSLog(@"Logged in successfully!");
}];
}];
@H_404_32@// 当按钮被点击时,执行login命令.
self.loginButton.rac_command = self.loginCommand;
Signals信号 也可以表示定时器,其他的UI事件,或者任何其他会随时间变化的东西.
在异步操作上使用signals信号,让通过链接和转换这些signal信号,构建更加复杂的行为成为可能.可以在一组操作完成后,来触发此操作即可:
// 执行两个网络操作,并在它们都完成后在控制台打印信息.
//
// +merge: 传入一组signal信号,并返回一个新的RACSignal信号对象.这个新返回的RACSignal信号对象,传递所有请求的值,并在所有的请求完成时完成.即:新返回的RACSignal信号,在每个请求完成时,都会发送个消息;在所有消息完成时,除了发送消息外,还会触发"完成"相关的block.
//
// -subscribeCompleted: signal信号完成时,将会执行block.
[[RACSignal merge:@[ [client fetchUserRepos],[client fetchOrgRepos] ]]
subscribeCompleted:^{
NSLog(@"They're both done!");
}];
Signals 信号可以被链接以连续执行异步操作,而不再需要嵌套式的block调用.用法类似于:
@H_404_32@// 用户登录,然后加载缓存信息,然后从服务器获取剩余的消息.在这一切完成后,输入信息到控制台.
@H_404_32@//
@H_404_32@// 假想的 -logInUser 方法,在登录完成后,返回一个signal信号对象.
@H_404_32@//
@H_404_32@// -flattenMap: 无论任何时候,signal信号发送一个值,它的block都将被执行,然后返回一个新的RACSignal,这个新的RACSignal信号对象会merge合并所有此block返回的signals信号为一个RACSignal信号对象.
[[[[client
logInUser]
flattenMap:^(User *user) {
@H_404_32@// Return a signal that loads cached messages for the user.
return [client loadCachedMessagesForUser:user];
}]
flattenMap:^(NSArray *messages) {
@H_404_32@// 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.");
}];
RAC 甚至让绑定异步操作的结果也更容易:
@H_404_32@// 创建一个单向的绑定,遮掩self.imagView.image就可以在用户的头像下载完成后自动被设置.
@H_404_32@//
@H_404_32@// 假定的 -fetchUserWithUsername: 方法返回一个发送用户对象的signal信号对象.
@H_404_32@//
@H_404_32@// -deliverOn: 创建一个新的 signals 信号对象,以在其他队列来处理他们的任务.
@H_404_32@// 在这个示例中,这个方法被用来将任务移到后台队列,并在稍后下载完成后返回主线程中.
@H_404_32@//
@H_404_32@// -map: 每个获取的用户都会传递进到这个block,然后返回新的RACSignal信号对象,这个
@H_404_32@// signal信号对象发送从这个block返回的值.
RAC(self.imageView,image) = [[[[client
fetchUserWithUsername:@"joshaber"]
deliverOn:[RACScheduler scheduler]]
map:^(User *user) {
@H_404_32@// 下载头像(这在后台执行.)
return [UIImage imageWithData: [NSData dataWithContentsOfURL: user.avatarURL]];
}]
@H_404_32@// 现在赋值在主线程完成.
deliverOn:RACScheduler.mainThreadScheduler];
何时使用 ReactiveCocoa ?
ReactiveCocoa 非常抽象,初次接触,通常很难理解如何使用它来解决具体的问题.
这是一些使用RAC更具有优势的应用场景:
处理异步或事件驱动的数据源.
大多说Cocoa程序的重心在于响应用户事件或程序状态的变化上.处理这些情况的代码,很快就会变得很复杂,就像意大利面条那样,拥有许多的回调和状态变量来处理顺序问题.
一些编程模式,表面上看有些相似,比如 UI回调方法,网络请求的响应和KVO通知等;实际上他们拥有许多的共同点. RACSignal 信号类,统一类这些不同的APIS,以便组合使用和操作它们.
例如,如下代码:
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];
}];
ReactiveCocoa 可以特别方便地处理这种逻辑模式:
[[[[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.");
}];
并行独立的工作.
使用独立数据的并行工作,然后最终将他们合并到一个结果中,在Cocoa中是很琐碎的,并且常常包含许多同步代码:
__block NSArray *databaSEObjects;
__block NSArray *fileContents;
NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init]; NSBlockOperation *databaSEOperation = [NSBlockOperation blockOperationWithBlock:^{ databaSEObjects = [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 finishProcessingDatabaSEObjects:databaSEObjects fileContents:fileContents];
NSLog(@"Done processing");
}];
[finishOperation addDependency:databaSEOperation];
[finishOperation addDependency:filesOperation];
[backgroundQueue addOperation:databaSEOperation];
[backgroundQueue addOperation:filesOperation];
[backgroundQueue addOperation:finishOperation];
以上代码可以通过复合使用signals信号对象来优化:
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 *databaSEObjects,NSArray *fileContents) {
[self finishProcessingDatabaSEObjects:databaSEObjects fileContents:fileContents];
return nil;
}]
subscribeCompleted:^{
NSLog(@"Done processing");
}];
简化集合的转换.
更高层级的排序函数,比如 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"];
}];