介绍
ReactiveObjC
的灵感来自函数式响应式编程。RAC
使用了捕获当前值和未来值的信号(RACSignal
),而没有使用替换和修改变量值的方式。
通过链接、组合和对信号响应的方式以声明的方式编写程序。
例如,一个输入框可以获取最新的时间,即使发什了改变,也不用使用额外的代码来监听时间和每秒更新文本内容。它的工作原理很像KVO
,但是是使用了block
代替-observeValueForKeyPath:ofObject:change:context:
方法。
信号也可以用来实现异步操作,就像futures
和promises
一样。这大大简化了异步操作,包括网络请求相关代码。
RAC
的一个主要优点是它提供了一个单一的、统一的方法来处理异步行为,包括delegate
、blocks
、target-action
机制、notifications
和KVO
。
简单例子:
// 当 self.username 改变, 打印最新的值
//
// RACObserve(self, username) 创建了一个新的信号量用来发送信号
// selg.username每当改变时就会有一个新值。
// 将在信号发送一个新值时执行subscribeNext相关block方法
[RACObserve(self, username) subscribeNext:^(NSString *newName) {
NSLog(@"%@", newName);
}];
但是和KVO
通知不同的是,Signals
可以组合并共同操作:
//只打印以“j”开头的名字
//
//-filter返回一个新的RACSignal,当它的内容返回YES时。
[[RACObserve(self, username)
filter:^(NSString *newName) {
return [newName hasPrefix:@"j"];
}]
subscribeNext:^(NSString *newName) {
NSLog(@"%@", newName);
}];
Signals
也可以用来获取状态。RAC
不需要观察属性并根据新值设置其他属性,而是可以用信号和操作来表示属性:
//创建一个单向绑定用判断self.createEnabled和self.password的值是否相等,若相等则self.createEnabled等于true
//RAC()是一个很好的绑定宏
//
//+combineLatest接收一个数组,用来表示当任何信号发送变化时,获取每个信号的最新值并返回一个信号量并将最新值返回。
RAC(self, createEnabled) = [RACSignal
combineLatest:@[ RACObserve(self, password), RACObserve(self, passwordConfirmation) ]
reduce:^(NSString *password, NSString *passwordConfirm) {
return @([passwordConfirm isEqualToString:password]);
}];
不止KVO
,Signals
可以随着时间改变建立在任何stream
值上。例如,它可以用来表示按下按钮:
// 当按下按钮时记录信息
//
// RACCommand 创建一个信号来表示UI操作。例如,每个信号可以表示按下按钮及与之相关的操作。
//
// -rac_command 是对NSButton的补充。每当按钮被按下时他就会发送命令给自己。
self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
NSLog(@"button was pressed!");
return [RACSignal empty];
}];
或者异步网络操作:
//点击登录按钮是进行网络请求
//
//当执行登录操作时,这个block将被运行,程序执行登录操作
self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) {
// 假设-login方法返回一个信号,当网络请求完成时返回一个值。
return [client logIn];
}];
//-executionSignals返回一个信号,该信号每当执行一次都有一个包含上述返回的信号
[self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) {
// 当登录成功时打印信息
[loginSignal subscribeCompleted:^{
NSLog(@"Logged in successfully!");
}];
}];
//按下登录按钮执行登录操作
self.loginButton.rac_command = self.loginCommand;
Signals
还可以用作定时器,其他UI
事件,或者任何随着时间变化的东西。
在异步操作中可以通过连接和转换这些信号来构建更为复杂的操作。在操作完成后可以轻松的触发:
//执行2个网络请求,完成后进行打印操作
//
// +merger:接收一个信号数组并返回一个新的RACSignal,该RACSignal传递所有信号的值,并在所有信号完成后进行完成。
//
//-subscribeCompleted:将在信号完成时执行
[[RACSignal
merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]]
subscribeCompleted:^{
NSLog(@"They're both done!");
}];
Signals
可以链接起来以顺序执行的方式进行异步操作,而不是用block
嵌套的方式回调。这类似于futures
和promise
的用法:
//记录用户,然后加载所有缓存的消息,然后从服务器获取剩余的消息。完成所有操作后,将一条消息记录到控制台。
//
//假设-logInUser方法返回一个在登录完成后的信号。
//
//-flattenMap:每当有一个信号发送值是,将执行该block代码,并返回一个将所有返回信号合并成单一信号的RACSignal。
[[[[client
logInUser]
flattenMap:^(User *user) {
// 返回一个用户加载缓存消息的信号。
return [client loadCachedMessagesForUser:user];
}]
flattenMap:^(NSArray *messages) {
// 返回一个获取任何剩余消息的信号。
return [client fetchMessagesAfterMessage:messages.lastObject];
}]
subscribeNext:^(NSArray *newMessages) {
NSLog(@"New messages: %@", newMessages);
} completed:^{
NSLog(@"Fetched all messages.");
}];
RAC
也可以很容易绑定异步操作的结果:
//创建一个self.imageView.image一单被下载就会被设置成为用户头像的单向绑定。
//
//假设-fetchUserWithUsername:方法返回一个发送给用户的信号。
//
//-deliverOn:创建一个在其他队列上工作的新信号。在本例中,它用于将工作转移到后台队列,然后返回到主线程。
//
//-map:对每个调用block的用户,返回一个新的RACSignal,发送block返回的值。
RAC(self.imageView, image) = [[[[client
fetchUserWithUsername:@"joshaber"]
deliverOn:[RACScheduler scheduler]]
map:^(User *user) {
// 下载角色(这是在后台队列中完成的)。
return [[NSImage alloc] initWithContentsOfURL:user.avatarURL];
}]
// 赋值将在主线程完成
deliverOn:RACScheduler.mainThreadScheduler];
何时使用ReactiveObjc
初次接触,ReactiveObjc
非常的抽象,很难想象如何将其运用于具体的问题。
以下是RAC
擅长解决问题的一些用例。
处理异步或是事件驱动的数据源
大部分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),
RACObserve(self, 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];
}];
ReactiveObjc
让这中模式变得特别容易:
[[[[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"];
}];
注:以上内容翻译于github官方文档,如有错误,欢迎指正。
网友评论