ReactiveCocoa自述:工作原理和應用
本文翻譯自GitHub上的開源框架ReactiveCocoa的readme,
英文原文鏈接https://github.com/ReactiveCocoa/ReactiveCocoa.
ReactiveCocoa (RAC)是一個Objective-C的框架,它的靈感來自函數式響應式編程.
如果你已經很熟悉函數式響應式編程編程或者了解ReactiveCocoa的一些基本前提,check outDocumentation文件夾作為框架的概述,這里面有一些關于它怎么工作的深層次的信息.
感謝Rheinfabrik對ReactiveCocoa 3!_開發慷慨地贊助.
什么是ReactiveCocoa?
ReactiveCocoa文檔寫得很厲害,并且詳細地介紹了RAC是什么以及它是怎么工作的?
如果你多學一點,我們推薦下面這些資源:
- </li>
- </li>
- </li>
- </li>
- </li>
-
Previously answered Stack Overflow questions and GitHub issues
</li> -
The rest of the Documentation folder
</li> -
Functional Reactive Programming on iOS(eBook)
</li> </ol>如果你有任何其他的問題,請隨意提交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資源:
- </li>
- </li>
- </li>
- </li>
- </li>
-
Programming Reactive Extensions and LINQ
</li> </ul>RAC和Rx靈感都是來自函數式響應式編程.這里有些關于FRP(functional reactive programming)相關的資源: