本文翻譯自GitHub上的開源框架ReactiveCocoa的readme,
英文原文鏈接https://github.com/ReactiveCocoa/ReactiveCocoa.
ReactiveCocoa (RAC)是一個Objective-C的框架,它的靈感來自函數式響應式編程.
如果你已經很熟悉函數式響應式編程編程或者了解ReactiveCocoa的一些基本前提,check outDocumentation文件夾作為框架的概述,這裡面有一些關於它怎麼工作的深層次的信息.
感謝Rheinfabrik對ReactiveCocoa 3!_開發慷慨地贊助.
什麼是ReactiveCocoa?
ReactiveCocoa文檔寫得很厲害,並且詳細地介紹了RAC是什麼以及它是怎麼工作的?
如果你多學一點,我們推薦下面這些資源:
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)
如果你有任何其他的問題,請隨意提交issue,
file an 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資源:
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靈感都是來自函數式響應式編程.這裡有些關於FRP(functional reactive programming)相關的資源:
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