我是前言
這是我在簡書上的第一篇文章,目的是為了記錄一些學習過的知識,以供日后復習。當然,如果本人的一些文章能夠幫助到一些人更快、更好的完成工作或者知識擴充,本人感到非常榮幸。之前也想過寫一些東西,不過因為種種原因,未能執(zhí)行,本人會抽出時間回顧和修正文章中的各種問題,如果有什么疑問或者紕漏,歡迎指正。
關于本文
本文主要是學習raywenderlich的Reactive Cocoa tutorial系列教程,并加以個人理解。作者Colin Eberhardt有不少好的教程值得大家學習,話說raywenderlich真心是一個不錯的網站,希望沒有聽說過或者聽說過沒有學習過的小伙伴們進去轉一轉,說不定會有所收獲。廢話不多所,進入主題。
Whats Reactive Cocoa?
Reactive Cocoa(也叫做RAC,下文統(tǒng)稱RAC)是一個支持FRP(函數(shù)響應式編程)的框架,其靈感來自這篇博客。作為iOSer,我們寫的每一行代碼都是為了得到一個輸出,比如說一個按鈕點擊之后,處理相應的事件;一個UISearchBar的文本改變,提示不同的信息;用戶下拉刷新,獲取網絡數(shù)據(jù)以展示...所有的事件都是為了得到一個輸出(結果)... 簡單輸入輸出,可能像“Hello world”一樣,而復雜的輸出,可能要做一系列的操作之后,得到一系列的結果。
Q:在Cocoa的世界中,為了得到這個(些)結果,我們都采取了什么方式呢?
A:Target Action;Blocks;Delegations;Notifications;KVO;Threads and so on..
如果設計模式沒用好,架構沒弄好,那么我們的代碼常??雌饋砭拖袷敲鏃l一樣,難懂又難看。在RAC的世界中,以事件流(用Signal、SignalProductor來表示)的形式,組合和轉換信號,最終得到我們想要的輸出,代碼大統(tǒng)一。
Usage
目前的RAC支持OC和Swift兩個版本,前一段時間寫了一段swift,不過現(xiàn)在的swift像外掛一樣,打出來的IPA包比OC的大很多很多,所以在這里,暫時不提swift版本,日后有機會回來補上。
Round 1 UIControl Target Actions
需求來了:那個xxx啊,我們開個會,關于這個注冊功能的。balabalabala...大概設計成這樣

界面比較簡單,一個常規(guī)的注冊頁面。要求:用戶名長度必須大于3,密碼長度大于5,否則textField背景色為黃色,正確則為白色;密碼框內的內容要和確認密碼框的內容一致,否則確認密碼框的背景色為黃色;若上述條件都滿足,注冊按鈕可以點擊,否則不可點擊。
規(guī)則簡單,也很合理,那么我們需要怎么做呢?
這個簡單,看我的:
方案1(With out RAC):
Storyboard中大概這樣:

#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, weak, nullable) IBOutlet UITextField *userNameField;
@property (nonatomic, weak, nullable) IBOutlet UITextField *passwordField;
@property (nonatomic, weak, nullable) IBOutlet UITextField *confirmField;
@property (nonatomic, weak, nullable) IBOutlet UIButton *signUpButton;
@end
@implementation ViewController
- (IBAction)didChangedUserNameFieldEditing:(id)sender {
self.userNameField.backgroundColor = [self isValidUserName] ? [UIColor whiteColor] : [UIColor yellowColor];
self.signUpButton.enabled = [self shouldSignUp];
}
- (IBAction)didChangedPasswordFieldEditing:(id)sender {
self.passwordField.backgroundColor = [self isValidPassword] ? [UIColor whiteColor] : [UIColor yellowColor];
self.confirmField.backgroundColor = [self isValidConfirm] ? [UIColor whiteColor] : [UIColor yellowColor];
self.signUpButton.enabled = [self shouldSignUp];
}
- (IBAction)didChangedConfirmFieldEditing:(id)sender {
self.confirmField.backgroundColor = [self isValidConfirm] ? [UIColor whiteColor] : [UIColor yellowColor];
self.signUpButton.enabled = [self shouldSignUp];
}
- (BOOL)isValidUserName {
return self.userNameField.text.length > 3;
}
- (BOOL)isValidPassword {
return self.passwordField.text.length > 5;
}
- (BOOL)isValidConfirm {
return [self isValidPassword] && [self.confirmField.text isEqualToString:self.passwordField.text];
}
- (BOOL)shouldSignUp {
return [self isValidUserName] && [self isValidPassword] && [self isValidConfirm];
}
@end
思路很簡單:
- userNameField邏輯最簡單,根據(jù)用戶名的長度,修改其輸入框背景顏色,判斷注冊按鈕的enable狀態(tài),對其更新;
- 用戶輸入的密碼如果符合規(guī)則,修改passwordField背景色。同時,如果confirmField符合規(guī)則,更新confirmField的背景色,判斷注冊按鈕的enable狀態(tài),對其更新;
- 判斷confirmField的文字,修改其背景色,判斷注冊按鈕的enable狀態(tài),對其更新;
- 點擊signUpButton,獲得龍蝦一只。
可以發(fā)現(xiàn),在每一個TextField文本改變的時候,我們都要去判斷并更新signUpButton的enable狀態(tài),并且在編輯passwordField文本的時候,判斷并更新confirmField的背景色;
來看看如果使用RAC怎么搞呢?
方案2 (With in RAC) :
#import "ViewController.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
@interface ViewController ()
@property (nonatomic, weak, nullable) IBOutlet UITextField *userNameField;
@property (nonatomic, weak, nullable) IBOutlet UITextField *passwordField;
@property (nonatomic, weak, nullable) IBOutlet UITextField *confirmField;
@property (nonatomic, weak, nullable) IBOutlet UIButton *signUpButton;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
RACSignal *userNameSignal = [self.userNameField.rac_textSignal map:^id(NSString *text) {
return @(isValidInput(text, 3));
}];
RACSignal *passwordSignal = [self.passwordField.rac_textSignal map:^id(NSString *text) {
return @(isValidInput(text, 5));
}];
RACSignal *confirmSignal = [RACSignal combineLatest:@[passwordSignal, self.confirmField.rac_textSignal] reduce:^id(NSNumber *passwordValid, NSString *text) {
return @([text isEqualToString:self.passwordField.text] && passwordValid.boolValue);
}];
[userNameSignal subscribeNext:^(NSNumber *valid) {
self.userNameField.backgroundColor = colorWithFlag(valid.boolValue);
}];
[passwordSignal subscribeNext:^(NSNumber *valid) {
self.passwordField.backgroundColor = colorWithFlag(valid.boolValue);
}];
[confirmSignal subscribeNext:^(NSNumber *flag) {
self.confirmField.backgroundColor = colorWithFlag(flag.boolValue);
}];
[[RACSignal
combineLatest:@[userNameSignal, passwordSignal, confirmSignal] reduce:^id(NSNumber *userNameValid, NSNumber *passwordValid, NSNumber *confirmValid) {
return @(userNameValid.boolValue && passwordValid.boolValue && confirmValid.boolValue);
}] subscribeNext:^(NSNumber *allValid) {
self.signUpButton.enabled = allValid.boolValue;
}];
[self.signUpButton rac_signalForControlEvents:UIControlEventTouchUpInside];
}
BOOL isValidInput(NSString *input, NSUInteger givenLength) {
return input.length > givenLength;
}
UIColor * colorWithFlag(BOOL flag) {
return flag ? [UIColor whiteColor] : [UIColor yellowColor];
}
@end
可以看到,用了RAC之后,我們不需要再去寫action,或者IBOutlet,通篇都是一些Signal之類的東西。我大概的思路是這樣的:
- 觀察userNameField的
rac_textSignal(RAC對許多類都有其不同的Signal,如果感興趣可以去看一看),textSignal、textSignal,顧名思義,一個文本事件流,發(fā)出的信號是一個NSString *類型的對象,然后通過map:方法將信號轉換成一個BOOL含義的NSNumber標識(我們發(fā)出、轉換或者合并的信號量都是NSObject類型,如果需要返回基本類型,例如此處的BOOL類型,需要將基本類型升級成對象,例如此處的NSNumber *),然后通過subscirbeNext:方法訂閱(個人理解為接收一個事件流,事件流發(fā)出的信號,在訂閱期間可以接收到)這個信號,如果接收到的信號為真,代表合法的用戶名,改變userNameField背景色為白色,否則背景色為黃色。passwordField同理; - confirmField只有在密碼合法并且其文本與passwordField的文本相同時,才改變其顏色為白色,否則為黃色,用RAC該怎么處理呢?在這里,我選擇了combine(組合)的方式。
- 將confirmField.rac_textSignal與passwordSignal通過
combineLatest:方法組合成一個新的事件流confirmSignal,也就是說,用戶每次編輯passwordField或者confirmField的時候,我們都能接收到confirmSignal發(fā)出BOOL含義的NSNumber *信號,然后訂閱這個事件流,通過信號來更新其背景色;
- 那么更新signUpButton的邏輯同上,將三個事件流組合成一個新的事件流,訂閱事件流,根據(jù)信號來更新enable狀態(tài);
- 事件流的處理到此為止,接下來,我們要點擊注冊按鈕的時候,push一個頁面,得到我們想要的龍蝦,既然不使用target action的方式,那么RAC給我們提供了一個 [UIButton rac_signalForControlEvents:]方法來注冊一個事件流,這里我們并沒有對其處理,所以寫成
[self.signUpButton rac_signalForControlEvents:UIControlEventTouchUpInside];
#######個人理解:我認為,RAC這樣寫的好處,在訂閱每一個事件流的時候,只處理這一個事件,不做多余的操作和判斷;當多個事件決定同一個結果時,可以將事件流組合,而不會將邏輯拆分到各個action、delegate..中;統(tǒng)一我們的各種事件處理;更好的支持函數(shù)式編程;提高了程序的可讀性和可維護性,更多的好處期待大家一起來發(fā)掘。
尾聲咯
差不多邏輯就是這樣了,但是我們并沒有處理block的retain-cycle,RAC提供了兩個很有意思的macro: @weakify(), @strongify(),會大大幫助我們減少代碼量,相信如果沒有這兩個宏,RAC寫起來也會很難看,可以自行Google。
還有一件事,每次寫這句話,就好像<成龍歷險記>中的老爹...
我們的RAC推薦這樣的寫法
self.userNameField.rac_textSignal
__filter: ...(這個是干嘛的,自己看看唄,這個玩意兒可以用來簡化我們的代碼的)
__map: ....
__reduce: ...
__subscribeNext:... (PS: 前面的雙下劃線代表空格,第一次用markdown,下班時間,憑記憶用的還是..請見諒)
__...
關于第一部分差不多就說這么多了,如果有時間第二部分一定盡快奉上
(由于之前的2B行為,整理了一天git,項目代碼放到這里,感興趣的可以看看)。
I'm Chris, an iOSer. 歡迎討論,微博@叫Chris真難。:)