前言
此為ReactiveObjC文檔翻譯,gitHub tag:2.12 date:2017-03-15 QQ: 809199658.
原文地址:ReactiveCocoa/ReactiveObjC(四級險過翻譯,請諒解)
ReactiveObjC
提示:本文是使用老版本ReactiveCocoa - Objective-C的介紹,Objective-C ReactiveCocoa又名ReactiveObjC,如果希望使用新版(swift)的ReactiveCocoa,可以查看ReactiveCocoa或者ReactiveSwift.
ReactiveObjC (一般來說又叫做ReactiveCocoa或者RAC)是一個基于響應式編程的Objective-C的框架.使用它提供API用以創(chuàng)建和改變數據流.
如果你已經熟悉響應式編程或者了解基本的ReactiveObjC概念,你可以查看github上的其他文件來了解其框架的概述然后在實踐中深入了解其使用
新接觸ReactiveObjC?
ReactiveObjC的文檔非常完善,并且有非常豐富的介紹材料用以了解并使用RAC.
如果你想學習更多,我們推薦你下列的這些資源
- introduction
- when-to-use-reactiveobjc
- Framework Overview
- Basic Operators
- Header Documentation
- 在Stack Overflow之前問題的回答或者GitHub issues.
- 本文章的剩余部分
- Functional Reactive Programming on iOS (eBook)
如果你還有更多問題,請提交issue
介紹
ReactiveObjC是基于函數響應式編程,相對于修改或替換現有變量,RAC提供信號類(代表:RACSugnal)來捕捉現有和 未來(future???)的變量
通過鏈接,綁定以及響應信號,不需要監(jiān)聽來更新變量,使得程序更加簡潔
例如,一個文本框被綁定后,相對于使用額外的代碼去監(jiān)聽時間和更新文本框,使用block來重寫RAC使得其更像KVO
-observeValueForKeyPath:ofObject:change:context:
信號類也可以像多線程一樣使用,像并發(fā)式編程.這簡化了包括在網絡請求在內的多線程程序.
RAC的最大優(yōu)點就是它提供了一個信號,統(tǒng)一了解決異步行為的方法,包括代理方法,target-action機制,通知,和KVO.
例1:
// 當self.username 改變時,控制臺會輸出他的新名字
//
// RACObserve(self, username)在文本改變的時候創(chuàng)建了發(fā)送當前姓名的一個新信號
// -subscribeNext: 在收到信號后執(zhí)行block
[RACObserve(self, username) subscribeNext:^(NSString *newName) {
NSLog(@"%@", newName);
}];
相對于KVO,信號可以被鏈接到一起并一同執(zhí)行
//只打印以'j'開頭的名字
//
// 過濾器在第一個信號block被調用返回YES時才發(fā)送一個新值給下一個block
[[RACObserve(self, username)
filter:^(NSString *newName) {
return [newName hasPrefix:@"j"];
}]
subscribeNext:^(NSString *newName) {
NSLog(@"%@", newName);
}];
信號也可以用來獲取狀態(tài),RAC可以將屬性快速的轉換成信號和操作來替代監(jiān)聽屬性然后改變值得這種方式.
//創(chuàng)建一個單程綁定,這樣當self.password和self.passwordConfirmation一樣時self.createEnabled將會變成ture
//
// RAC() 宏可以使得綁定表示的更清楚
//
// +combineLatest:reduce: 獲取一個信號數組,當其中任何一個改變時傳遞新值并調用這些blocks,并且返回block返回值的一個新RACSignal信號
//(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]);
}];
信號可以在任何時間任何數據流上創(chuàng)建,不僅限于KVO,例如,可以用于按鈕點擊事件
// 在按鈕點擊時打印信息
// RACCommand 用以表示一個UI事件. 每一個信號可以表示一次點擊, 例如,在點擊事件時處理額外的事件
// -rac_command 是 NSButton 的額外方法 . 在點擊時,按鈕將自身發(fā)送給它自己.
self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
NSLog(@"button was pressed!");
return [RACSignal empty];
}];
或者異步網絡請求
// 給按鈕綁定一個登錄事件請求網絡
//
// 在登錄命令執(zhí)行的時候這個block將用以執(zhí)行登錄程序
self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) {
// 這個登錄命令在網絡請求結束后返回發(fā)送值得一個信號
return [client logIn];
}];
// -executionSignals 返回一個包含上面block的信號,收到一次信號執(zhí)行一次
[self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) {
// 登錄成功調用
[loginSignal subscribeCompleted:^{
NSLog(@"Logged in successfully!");
}];
}];
// 按鈕點擊時執(zhí)行登錄命令
self.loginButton.rac_command = self.loginCommand;
信號也可以在timer,UI事件,或者其他隨時間改變的事情上
將多個信號鏈接在一起成為一組操作(a group of operations completes)可以使得更復雜的異步操作變得簡單
// 在兩個網絡請求結束后打印信息
//
// +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: 在信號結束后調用
[[RACSignal
merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]]
subscribeCompleted:^{
NSLog(@"They're both done!");
}];
多個異步操作也可以將信號鏈接實現,不同于回調block,這個和并發(fā)操作的一般做法一致.
// 登錄用戶,服務器獲取信息,獲取本地緩存信息,之后顯示全部信息
//
// hypothetical -logInUser 在登錄完成后返回完成信號
//
// -flattenMap: 在信號發(fā)送后執(zhí)行block, 返回一個合并所有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
//
//hypothetical -fetchUserWithUsername: 發(fā)送給用戶的信號
//
// -deliverOn: 在其他隊列上創(chuàng)建一個新的信號在本例中,
//它用來將任務放置在后臺隊列然后返回主隊列中
//
RAC(self.imageView, image) = [[[[client
fetchUserWithUsername:@"joshaber"]
deliverOn:[RACScheduler scheduler]]
map:^(User *user) {
// 在后臺下載頭像
return [[NSImage alloc] initWithContentsOfURL:user.avatarURL];
}]
// 此操作在主線程
deliverOn:RACScheduler.mainThreadScheduler];
上面這些例子表明了RAC可以做的事情,但是很難再README大小的例子中展示為什么RAC很犀利,但是可以表明RAC怎么樣讓代碼更清晰(吹牛中)....
如果要更多的代碼示例,可以查看C-41或者GroceryList.其中所有的iOS APP都是用RAC來實現的,更多關于RAC的信息都可以在其中找到.
什么時候使用RAC
初看的時候,RAC非常抽象,而且很難了解什么時候使用來解決問題.
接下來的demo是RAC擅長的領域.
操作異步或者事件驅動數據源
許多的cocoa程序關注于程序對事件的響應或改變,處理的代碼很可能變得非常復雜(就像意大利面條??一樣),非常多的回調block和狀態(tài)變量用來處理問題.
樣式(Patterns)看起來是突出的那個,像UI的回調,網絡回應,KVO,通知,其實他們都有許多一樣的地方,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可以被這樣實現
- (void)viewDidLoad {
[super viewDidLoad];
@weakify(self);
//將button的enable和accountText,passwordText,isLogin,logined綁定起來
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);
}];
//loginbutton 的點擊事件
[[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];
}];
使用RAC可以更簡單
[[[[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];
使用RAC可以簡化成如下代碼
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也可能在fondation框架中缺失或者導致loop-focused崩潰.
//在遍歷中添加數據導致無限循環(huán)
NSMutableArray *results = [NSMutableArray array];
for (NSString *str in strings) {
if (str.length < 2) {
continue;
}
NSString *newString = [str stringByAppendingString:@"foobar"];
[results addObject:newString];
}
RAC中RACSequence允許在遍歷中添加值.
RACSequence *results = [[strings.rac_sequence
filter:^ BOOL (NSString *str) {
return str.length >= 2;
}]
map:^(NSString *str) {
return [str stringByAppendingString:@"foobar"];
}];
系統(tǒng)要求
OS X 10.8+ 及iOS8.0+
導入ReactiveObjC
導入ReactiveObjC你可以
- 添加ReactiveObjC倉庫到你的程序倉庫中
- 在你的ReactiveObjC文件夾中運行
git submodule update --init --recursive - 將
ReactiveObjC.xcodeproj拖入你的Xcode項目中 - 在"Build Phases"中添加RAC到"Link Binary With Libraries"中
- 添加
ReactiveObjC.frameworkRAC也必須添加到任何"Copy Frameworks"build phase中.如果你還沒創(chuàng)建,簡單的添加一個"Copy Files" build phase指向(target)"Frameworks"位置 - 添加
"$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/include" $(inherited)到"Header Search Paths" build setting 中 - 如果是iOS,則需要在"Other Linker Flags" build setting添加
-ObjC
后記
四級渣翻譯,如果有什么意見或者改善的地方可以留言或者通過QQ聯(lián)系我~