ReactiveCocoa自述:工作原理和應(yīng)用
ReactiveCocoa (RAC)是一個(gè)Objective-C的框架,它的靈感來自函數(shù)式響應(yīng)式編程.
如果你已經(jīng)很熟悉函數(shù)式響應(yīng)式編程編程或者了解ReactiveCocoa的一些基本前提,check outDocumentation文件夾作為框架的概述,這里面有一些關(guān)于它怎么工作的深層次的信息.
什么是ReactiveCocoa?
ReactiveCocoa文檔寫得很厲害,并且詳細(xì)地介紹了RAC是什么以及它是怎么工作的?
如果你多學(xué)一點(diǎn),我們推薦下面這些資源:
Introduction
When to use ReactiveCocoa
Framework Overview
Basic Operators
Header documentation
Previously answered Stack Overflow questions and GitHub issues
The rest of the Documentation folder
Functional Reactive Programming on iOS(eBook)
如果你有任何其他的問題,請(qǐng)隨意提交issue,
file an issue.
介紹
ReactiveCocoa的靈感來自函數(shù)式響應(yīng)式編程.Rather than using mutable variables which are replaced and modified in-place,RAC提供signals(表現(xiàn)為RACSignal)來捕捉當(dāng)前以及將來的值.
通過對(duì)signals進(jìn)行連接,綁定和響應(yīng),不需要連續(xù)地觀察和更新值,軟件就能寫了.
舉個(gè)例子,一個(gè)text field能夠綁定到最新狀態(tài),即使它在變,而不需要用額外的代碼去更新text field每一秒的狀態(tài).它有點(diǎn)像KVO,但它用blocks代替了重寫-observeValueForKeyPath:ofObject:change:context:.
Signals也能夠呈現(xiàn)異步的操作,有點(diǎn)像futures and promises.這極大地簡化了異步軟件,包括了網(wǎng)絡(luò)處理的代碼.
RAC有一個(gè)主要的優(yōu)點(diǎn),就是提供了一個(gè)單一的,統(tǒng)一的方法去處理異步的行為,包括delegate方法,blocks回調(diào),target-action機(jī)制,notifications和KVO.
這里有一個(gè)簡單的例子:
- // 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能夠連接在一起并且能夠同時(shí)進(jìn)行操作:
- // 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也能夠用來導(dǎo)出狀態(tài).而不是observing properties或者設(shè)置其他的 properties去反應(yīng)新的值,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];
- }];
或者異步的網(wǎng)絡(luò)操作:
- // 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事件,或者其他跟時(shí)間改變有關(guān)的東西.
對(duì)于用signals來進(jìn)行異步操作,通過連接和改變這些signals能夠進(jìn)行更加復(fù)雜的行為.在一組操作完成時(shí),工作能夠很簡單觸發(fā):
- // 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能夠順序地執(zhí)行異步操作,而不是嵌套block回調(diào).這個(gè)和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也能夠簡單地綁定異步操作的結(jié)果:
- // 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為什么如此強(qiáng)大.雖然通過這個(gè)README很難說清RAC,但我盡可能用更少的代碼,更少的模版,把更好的代碼去表達(dá)清楚.
如果想要更多的示例代碼,可以check outC-41 或者 GroceryList,這些都是真正用ReactiveCocoa寫的iOS apps.更多的RAC信息可以看一下Documentation文件夾.
什么時(shí)候用ReactiveCocoa
乍看上去,ReactiveCocoa是很抽象的,它可能很難理解如何將它應(yīng)用到具體的問題.
這里有一些RAC常用的地方.
處理異步或者事件驅(qū)動(dòng)數(shù)據(jù)源
很多Cocoa編程集中在響應(yīng)user events或者改變application state.這樣寫代碼很快地會(huì)變得很復(fù)雜,就像一個(gè)意大利面,需要處理大量的回調(diào)和狀態(tài)變量的問題.
這個(gè)模式表面上看起來不同,像UI回調(diào),網(wǎng)絡(luò)響應(yīng),和KVO notifications,實(shí)際上有很多的共同之處。RACSignal統(tǒng)一了這些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表達(dá)的話就像下面這樣:
- - (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];
- }
連接依賴的操作
依賴經(jīng)常用在網(wǎng)絡(luò)請(qǐng)求,當(dāng)下一個(gè)對(duì)服務(wù)器網(wǎng)絡(luò)請(qǐng)求需要構(gòu)建在前一個(gè)完成時(shí),可以看一下下面的代碼:
- [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 則讓這種模式特別簡單:
- __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來清理和優(yōu)化:
- 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");
- }];
簡化集合轉(zhuǎn)換
像map, filter, fold/reduce 這些高級(jí)功能在Foundation中是極度缺少的m導(dǎo)致了一些像下面這樣循環(huán)集中的代碼:
- NSMutableArray *results = [NSMutableArray array];
- for (NSString *str in strings) {
- if (str.length < 2) {
- continue;
- }
- NSString *newString = [str stringByAppendingString:@"foobar"];
- [results addObject:newString];
- }
RACSequence能夠允許Cocoa集合用統(tǒng)一的方式操作:
- RACSequence *results = [[strings.rac_sequence
- filter:^ BOOL (NSString *str) {
- return str.length >= 2;
- }]
- map:^(NSString *str) {
- return [str stringByAppendingString:@"foobar"];
- }];
系統(tǒng)要求
ReactiveCocoa 要求 OS X 10.8+ 以及 iOS 8.0+.
引入 ReactiveCocoa
增加 RAC 到你的應(yīng)用中:
1. 增加 ReactiveCocoa 倉庫 作為你應(yīng)用倉庫的一個(gè)子模塊.
2. 從ReactiveCocoa文件夾中運(yùn)行 script/bootstrap .
3. 拖拽 ReactiveCocoa.xcodeproj 到你應(yīng)用的 Xcode project 或者 workspace中.
4. 在你應(yīng)用target的"Build Phases"的選項(xiàng)卡,增加 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到一個(gè)project (不是一個(gè)workspace), 你需要適當(dāng)?shù)奶砑覴AC target到你應(yīng)用的"Target Dependencies".
假如你喜歡用CocoaPods,這里有一些慷慨地第三方貢獻(xiàn)ReactiveCocoa podspecs .
想看一個(gè)用了RAC的工程,check outC-41 或者 GroceryList,這些是真實(shí)的用ReactiveCocoa寫的iOS apps.
獨(dú)立開發(fā)
假如你的工作用RAC是隔離的而不是將其集成到另一個(gè)項(xiàng)目,你會(huì)想打開ReactiveCocoa.xcworkspace 而不是.xcodeproj.
更多信息
ReactiveCocoa靈感來自.NET的ReactiveExtensions (Rx).Rx的一些原則也能夠很好的用在RAC.這里有些好的Rx資源:
Reactive Extensions MSDN entry
Reactive Extensions for .NET Introduction
Rx - Channel 9 videos
Reactive Extensions wiki
101 Rx Samples
Programming Reactive Extensions and LINQ
RAC和Rx靈感都是來自函數(shù)式響應(yīng)式編程.這里有些關(guān)于FRP(functional reactive programming)相關(guān)的資源:
What is FRP? - Elm Language
What is Functional Reactive Programming - Stack Overflow
Specification for a Functional Reactive Language - Stack Overflow
Escape from Callback Hell
Principles of Reactive Programming on Coursera