ReactiveCocoa自述:工作原理和应用
本文翻译自GitHub上的开源框架ReactiveCocoa的readme,
英文原文链接https://github.com/ReactiveCocoa/ReactiveCocoa.
ReactiveCocoa (RAC)是一个Objective-C的框架,它的灵感来自函数式响应式编程.
如果你已经很熟悉函数式响应式编程编程或者了解ReactiveCocoa的一些基本前提,check outDocumentation文件夹作为框架的概述,这里面有一些关于它怎么工作的深层次的信息.
感谢Rheinfabrik对ReactiveCocoa 3!_开发慷慨地赞助.
什么是ReactiveCocoa?
ReactiveCocoa文档写得很厉害,并且详细地介绍了RAC是什么以及它是怎么工作的?
如果你多学一点,我们推荐下面这些资源:
-
Previously answered Stack Overflow questions and GitHub issues
-
The rest of the Documentation folder
如果你有任何其他的问题,请随意提交issue,
介绍
ReactiveCocoa的灵感来自函数式响应式编程.Rather than using mutable variables which are replaced and modified in-place,RAC提供signals(表现为RACSignal)来捕捉当前以及将来的值.
通过对signals进行连接,绑定和响应,不需要连续地观察和更新值,软件就能写了.
举个例子,一个text field能够绑定到最新状态,即使它在变,而不需要用额外的代码去更新text field每一秒的状态.它有点像KVO,但它用blocks代替了重写-observeValueForKeyPath:ofObject:change:context:.
Signals也能够呈现异步的操作,有点像futures and promises.这极大地简化了异步软件,包括了网络处理的代码.
RAC有一个主要的优点,就是提供了一个单一的,统一的方法去处理异步的行为,包括delegate方法,blocks回调,target-action机制,notifications和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); }];
这不像KVO notifications,signals能够连接在一起并且能够同时进行操作:
// 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); }];
Signals也能够用来导出状态.而不是observing properties或者设置其他的 properties去反应新的值,RAC通过signals and operations让表示属性变得有可能:
// 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]); }];
Signals不仅仅能够用在KVO,还可以用在很多的地方.比如说,它们也能够展示button presses:
// 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]; }];
或者异步的网络操作:
// 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;
Signals能够展示timers,其他的UI事件,或者其他跟时间改变有关的东西.
对于用signals来进行异步操作,通过连接和改变这些signals能够进行更加复杂的行为.在一组操作完成时,工作能够很简单触发:
// 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!"); }];
Signals能够顺序地执行异步操作,而不是嵌套block回调.这个和futures and promises很相似:
// 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."); }];
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];
这里仅仅说了RAC能做什么,但很难说清RAC为什么如此强大.虽然通过这个README很难说清RAC,但我尽可能用更少的代码,更少的模版,把更好的代码去表达清楚.
如果想要更多的示例代码,可以check outC-41 或者 GroceryList,这些都是真正用ReactiveCocoa写的iOS apps.更多的RAC信息可以看一下Documentation文件夹.
什么时候用ReactiveCocoa
乍看上去,ReactiveCocoa是很抽象的,它可能很难理解如何将它应用到具体的问题.
这里有一些RAC常用的地方.
处理异步或者事件驱动数据源
很多Cocoa编程集中在响应user events或者改变application state.这样写代码很快地会变得很复杂,就像一个意大利面,需要处理大量的回调和状态变量的问题.
这个模式表面上看起来不同,像UI回调,网络响应,和KVO notifications,实际上有很多的共同之处。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]; }];
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 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中是极度缺少的m导致了一些像下面这样循环集中的代码:
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"]; }];
系统要求
ReactiveCocoa 要求 OS X 10.8+ 以及 iOS 8.0+.
引入 ReactiveCocoa
增加 RAC 到你的应用中:
1. 增加 ReactiveCocoa 仓库 作为你应用仓库的一个子模块.
2. 从ReactiveCocoa文件夹中运行 script/bootstrap .
3. 拖拽 ReactiveCocoa.xcodeproj 到你应用的 Xcode project 或者 workspace中.
4. 在你应用target的"Build Phases"的选项卡,增加 RAC到 "Link Binary With Libraries"
On iOS, 增加 libReactiveCocoa-iOS.a.
On OS X, 增加 ReactiveCocoa.framework.
RAC 必须选择"Copy Frameworks" . 假如你没有的话, 需要选择"Copy Files"和"Frameworks" .
5. 增加 "$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/include"
$(inherited)到 "Header Search Paths" (这需要archive builds, 但也没什么影响).
6. For iOS targets, 增加 -ObjC 到 "Other Linker Flags" .
7. 假如你增加 RAC到一个project (不是一个workspace), 你需要适当的添加RAC target到你应用的"Target Dependencies".
假如你喜欢用CocoaPods,这里有一些慷慨地第三方贡献ReactiveCocoa podspecs .
想看一个用了RAC的工程,check outC-41 或者 GroceryList,这些是真实的用ReactiveCocoa写的iOS apps.
独立开发
假如你的工作用RAC是隔离的而不是将其集成到另一个项目,你会想打开ReactiveCocoa.xcworkspace 而不是.xcodeproj.
更多信息
ReactiveCocoa灵感来自.NET的ReactiveExtensions (Rx).Rx的一些原则也能够很好的用在RAC.这里有些好的Rx资源:
RAC和Rx灵感都是来自函数式响应式编程.这里有些关于FRP(functional reactive programming)相关的资源:
来源:coderyi