ReactiveCocoa是Github開(kāi)源的一款cocoa FRP 框架,我在之前的文章里有過(guò)介紹(當(dāng)時(shí)還是1.x版本,2.x版本有了新的變化,API也有部分不兼容) 這里再簡(jiǎn)單地提一下。
Native
app有很大一部分的時(shí)間是在等待事件發(fā)生,然后響應(yīng)事件,比如等待網(wǎng)絡(luò)請(qǐng)求完成,等待用戶(hù)的操作,等待某些狀態(tài)值的改變等等,等這些事件發(fā)生后,再做進(jìn)一步處理。
但是這些等待和響應(yīng),并沒(méi)有一個(gè)統(tǒng)一的處理方式。Delegate, Notification, Block, KVO,
常常會(huì)不知道該用哪個(gè)最合適。有時(shí)需要chain或者compose某幾個(gè)事件,就需要多個(gè)狀態(tài)變量,而狀態(tài)變量一多,復(fù)雜度也就上來(lái)了。為了解決這些問(wèn)題,Github的工程師們開(kāi)發(fā)了ReactiveCocoa。
幾個(gè)常見(jiàn)的概念
在閱讀ReactiveCocoa(以下簡(jiǎn)稱(chēng)RAC)的相關(guān)文章或代碼時(shí),經(jīng)常會(huì)出現(xiàn)一些名詞,理解它們對(duì)于理解RAC有很大的幫助,下面就簡(jiǎn)要來(lái)說(shuō)說(shuō)這些常見(jiàn)的概念。
Signal and Subscriber
這是RAC最核心的內(nèi)容,這里我想用插頭和插座來(lái)描述,插座是Signal,插頭是Subscriber。想象某個(gè)遙遠(yuǎn)的星球,他們的電像某種物質(zhì)一樣被集中存儲(chǔ),且很珍貴。插座負(fù)責(zé)去獲取電,插頭負(fù)責(zé)使用電,而且一個(gè)插座可以插任意數(shù)量的插頭。當(dāng)一個(gè)插座(Signal)沒(méi)有插頭(Subscriber)時(shí)什么也不干,也就是處于冷(Cold)的狀態(tài),只有插了插頭時(shí)才會(huì)去獲取,這個(gè)時(shí)候就處于熱(Hot)的狀態(tài)。
Signal獲取到數(shù)據(jù)后,會(huì)調(diào)用Subscriber的sendNext, sendComplete,
sendError方法來(lái)傳送數(shù)據(jù)給Subscriber,Subscriber自然也有方法來(lái)獲取傳過(guò)來(lái)的數(shù)據(jù),如:[signal
subscribeNext:error:completed]。這樣只要沒(méi)有sendComplete和sendError,新的值就會(huì)通過(guò)sendNext源源不斷地傳送過(guò)來(lái),舉個(gè)簡(jiǎn)單的例子:
[RACObserve(self,username)subscribeNext:^(NSString*newName){NSLog(@"newName:%@",newName);}];
RACObserve使用了KVO來(lái)監(jiān)聽(tīng)property的變化,只要username被自己或外部改變,block就會(huì)被執(zhí)行。但不是所有的property都可以被RACObserve,該property必須支持KVO,比如NSURLCache的currentDiskUsage就不能被RACObserve。
Signal是很靈活的,它可以被修改(map),過(guò)濾(filter),疊加(combine),串聯(lián)(chain),這有助于應(yīng)對(duì)更加復(fù)雜的情況,比如:
RAC(self.logInButton,enabled)=[RACSignalcombineLatest:@[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);}];
這段代碼看起來(lái)有點(diǎn)復(fù)雜,來(lái)細(xì)細(xì)說(shuō)一下,首先是左邊的RAC(...),它的作用是將self.logInButton.enabled屬性與右邊的signal的sendNext值綁定。也就是如果右邊的reduce的返回值為NO,那么enabled就為NO。右邊的combineLatest是獲取這4個(gè)signal的next值。其中可以看到self.usernameTextField.rac_textSignal這么個(gè)東東,rac_textSignal是RAC為UITextField添加的category,只要usernameTextField的值有變化,這個(gè)值就會(huì)被返回(sendNext)。combineLatest需要每個(gè)signal至少都有過(guò)一次sendNext。reduce的作用是根據(jù)接收到的值,再返回一個(gè)新的值,這里是@(YES)和@(NO),必須是object。
上面這段代碼用到了Signal的組合,想象一下,如果是傳統(tǒng)的方式,寫(xiě)起來(lái)還是挺復(fù)雜的,而且隨著功能的增加,調(diào)整起來(lái)會(huì)更加麻煩。
冷信號(hào)(Cold)和熱信號(hào)(Hot)
上面提到過(guò)這兩個(gè)概念,冷信號(hào)默認(rèn)什么也不干,比如下面這段代碼
RACSignal*signal=[RACSignalcreateSignal:^RACDisposable*(id
subscriber){NSLog(@"triggered");[subscribersendNext:@"foobar"];[subscribersendCompleted];returnnil;}];
我們創(chuàng)建了一個(gè)Signal,但因?yàn)闆](méi)有被subscribe,所以什么也不會(huì)發(fā)生。加了下面這段代碼后,signal就處于Hot的狀態(tài)了,block里的代碼就會(huì)被執(zhí)行。
[signalsubscribeCompleted:^{NSLog(@"subscription %u",subscriptions);}];
或許你會(huì)問(wèn),那如果這時(shí)又有一個(gè)新的subscriber了,signal的block還會(huì)被執(zhí)行嗎?這就牽扯到了另一個(gè)概念:Side Effect
Side Effect
還是上面那段代碼,如果有多個(gè)subscriber,那么signal就會(huì)又一次被觸發(fā),控制臺(tái)里會(huì)輸出兩次triggered。這或許是你想要的,或許不是。如果要避免這種情況的發(fā)生,可以使用replay方法,它的作用是保證signal只被觸發(fā)一次,然后把sendNext的value存起來(lái),下次再有新的subscriber時(shí),直接發(fā)送緩存的數(shù)據(jù)。
Cocoa Categories
為了更加方便地使用RAC,RAC給Cocoa添加了很多category,與系統(tǒng)集成地越緊密,使用起來(lái)自然也就越方便。下面是我認(rèn)為比較常用的categories。
UIView Categories
上面看到的rac_textSignal是加在UITextField上的(UITextField+RACSignalSupport.h),其他常用的UIView也都有添加相應(yīng)的category,比如UIAlertView,就不需要再用Delegate了。
UIAlertView*alertView=[[UIAlertViewalloc]initWithTitle:@""message:@"Alert"delegate:nilcancelButtonTitle:@"YES"otherButtonTitles:@"NO",nil];[[alertViewrac_buttonClickedSignal]subscribeNext:^(NSNumber*indexNumber){if([indexNumberintValue]==1){NSLog(@"you touched NO");}else{NSLog(@"you touched YES");}}];[alertViewshow];
有了這些Category,大部分的Delegate都可以使用RAC來(lái)做。或許你會(huì)想,可不可以subscribe
NSMutableArray.rac_sequence.signal,這樣每次有新的object或舊的object被移除時(shí)都能知道,UITableViewController就可以根據(jù)dataSource的變化,來(lái)reloadData。但很可惜這樣不行,因?yàn)镽AC是基于KVO的,而NSMutableArray并不會(huì)在調(diào)用addObject或removeObject時(shí)發(fā)送通知,所以不可行。不過(guò)可以使用NSArray作為UITableView的dataSource,只要dataSource有變動(dòng)就換成新的Array,這樣就可以了。
說(shuō)到UITableView,再說(shuō)一下UITableViewCell,RAC給UITableViewCell提供了一個(gè)方法:rac_prepareForReuseSignal,它的作用是當(dāng)Cell即將要被重用時(shí),告訴Cell。想象Cell上有多個(gè)button,Cell在初始化時(shí)給每個(gè)button都addTarget:action:forControlEvents,被重用時(shí)需要先移除這些target,下面這段代碼就可以很方便地解決這個(gè)問(wèn)題:
[[[self.cancelButtonrac_signalForControlEvents:UIControlEventTouchUpInside]takeUntil:self.rac_prepareForReuseSignal]subscribeNext:^(UIButton*x){// do other things}];
還有一個(gè)很常用的category就是UIButton+RACCommandSupport.h,它提供了一個(gè)property:rac_command,就是當(dāng)button被按下時(shí)會(huì)執(zhí)行的一個(gè)命令,命令被執(zhí)行完后可以返回一個(gè)signal,有了signal就有了靈活性。比如點(diǎn)擊投票按鈕,先判斷一下有沒(méi)有登錄,如果有就發(fā)HTTP請(qǐng)求,沒(méi)有就彈出登陸框,可以這么實(shí)現(xiàn)。
voteButton.rac_command=[[RACCommandalloc]initWithEnabled:self.viewModel.voteCommand.enabledsignalBlock:^RACSignal*(idinput){// Assume that we're logged in at first. We'll replace this signal later if not.RACSignal*authSignal=[RACSignalempty];if([[PXRequestapiHelper]authMode]==PXAPIHelperModeNoAuth){// Not logged in. Replace signal.authSignal=[[RACSignalcreateSignal:^RACDisposable*(id
subscriber){@strongify(self);FRPLoginViewController*viewController=[[FRPLoginViewControlleralloc]initWithNibName:@"FRPLoginViewController"bundle:nil];UINavigationController*navigationController=[[UINavigationControlleralloc]initWithRootViewController:viewController];[selfpresentViewController:navigationControlleranimated:YEScompletion:^{[subscribersendCompleted];}];returnnil;}]];}return[authSignalthen:^RACSignal*{@strongify(self);return[[self.viewModel.voteCommandexecute:nil]ignoreValues];}];}];[voteButton.rac_command.errorssubscribeNext:^(idx){[xsubscribeNext:^(NSError*error){[SVProgressHUDshowErrorWithStatus:[errorlocalizedDescription]];}];}];
這段代碼節(jié)選自AshFurrow的FunctionalReactivePixels,有刪減。
Data Structure Categories
常用的數(shù)據(jù)結(jié)構(gòu),如NSArray, NSDictionary也都有添加相應(yīng)的category,比如NSArray添加了rac_sequence,可以將NSArray轉(zhuǎn)換為RACSequence,順便說(shuō)一下RACSequence,RACSequence是一組immutable且有序的values,不過(guò)這些values是運(yùn)行時(shí)計(jì)算的,所以對(duì)性能提升有一定的幫助。RACSequence提供了一些方法,如array轉(zhuǎn)換為NSArray,any:檢查是否有Value符合要求,all:檢查是不是所有的value都符合要求,這里的符合要求的,block返回YES,不符合要求的就返回NO。
NotificationCenter Category
NSNotificationCenter, 默認(rèn)情況下NSNotificationCenter使用Target-Action方式來(lái)處理Notification,這樣就需要另外定義一個(gè)方法,這就涉及到編程領(lǐng)域的兩大難題之一:起名字。有了RAC,就有Signal,有了Signal就可以subscribe,于是NotificationCenter就可以這么來(lái)處理,還不用擔(dān)心移除observer的問(wèn)題。
[[[NSNotificationCenterdefaultCenter]rac_addObserverForName:@"MyNotification"object:nil]subscribeNext:^(NSNotification*notification){NSLog(@"Notification Received");}];
NSObject Categories
NSObject有不少的Category,我覺(jué)得比較有用的有這么幾個(gè)
NSObject+RACDeallocating.h
顧名思義就是在一個(gè)object的dealloc被觸發(fā)時(shí),執(zhí)行的一段代碼。
NSArray*array=@[@"foo"];[[arrayrac_willDeallocSignal]subscribeCompleted:^{NSLog(@"oops, i will be gone");}];array=nil;
NSObject+RACLifting.h
有時(shí)我們希望滿足一定條件時(shí),自動(dòng)觸發(fā)某個(gè)方法,有了這個(gè)category就可以這么辦
-(void)test{RACSignal*signalA=[RACSignalcreateSignal:^RACDisposable*(id
subscriber){doubledelayInSeconds=2.0;dispatch_time_tpopTime=dispatch_time(DISPATCH_TIME_NOW,(int64_t)(delayInSeconds*NSEC_PER_SEC));dispatch_after(popTime,dispatch_get_main_queue(),^(void){[subscribersendNext:@"A"];});returnnil;}];RACSignal*signalB=[RACSignalcreateSignal:^RACDisposable*(idsubscriber){[subscribersendNext:@"B"];[subscribersendNext:@"Another B"];[subscribersendCompleted];returnnil;}];[selfrac_liftSelector:@selector(doA:withB:)withSignals:signalA,signalB,nil];}-(void)doA:(NSString*)AwithB:(NSString*)B{NSLog(@"A:%@ and B:%@",A,B);}
這里的rac_liftSelector:withSignals就是干這件事的,它的意思是當(dāng)signalA和signalB都至少sendNext過(guò)一次,接下來(lái)只要其中任意一個(gè)signal有了新的內(nèi)容,doA:withB這個(gè)方法就會(huì)自動(dòng)被觸發(fā)。
如果你有興趣,可以想想上面這段代碼會(huì)輸出什么。
NSObject+RACSelectorSignal.h
這個(gè)category有rac_signalForSelector:和rac_signalForSelector:fromProtocol:這兩個(gè)方法。先來(lái)看前一個(gè),它的意思是當(dāng)某個(gè)selector被調(diào)用時(shí),再執(zhí)行一段指定的代碼,相當(dāng)于hook。比如點(diǎn)擊某個(gè)按鈕后,記個(gè)日志。后者表示該selector實(shí)現(xiàn)了某個(gè)協(xié)議,所以可以用它來(lái)實(shí)現(xiàn)Delegate。
MVVM
RAC帶來(lái)的變化還不僅僅是這些,它還帶來(lái)了架構(gòu)層面的變化。我們都知道蘋(píng)果推薦的是MVC架構(gòu),那MVVM又是什么呢?
跟MVC最大的區(qū)別是多了個(gè)ViewModel,它直接與View綁定,而且對(duì)View一無(wú)所知。拿做菜打比方的話,ViewModel就是調(diào)料,它不關(guān)心做的到底是什么菜。這不是跟Model很像嗎?是的,它可以扮演Model的職責(zé),但其實(shí)它是Model的中介,這樣當(dāng)Model的API有變化,或者由本地存儲(chǔ)變?yōu)檫h(yuǎn)程API調(diào)用時(shí),ViewModel的public API可以保持不變。
使用ViewModel的好處是,可以讓Controller更加簡(jiǎn)單和輕便,而且ViewModel相對(duì)獨(dú)立,也更加方便測(cè)試和重用。那Controller這時(shí)又該做哪些事呢?在MVVM體系中,Controller可以被看成View,所以它的主要工作是處理布局、動(dòng)畫(huà)、接收系統(tǒng)事件、展示UI。
MVVM還有一個(gè)很重要的概念是data binding,view的呈現(xiàn)需要data,這個(gè)data就是由ViewModel提供的,將view的data與ViewModel的data綁定后,將來(lái)雙方的數(shù)據(jù)只要一方有變化,另一方就能收到。這里有Github 開(kāi)源的一個(gè)ViewModel Base Class。
其他
RAC在使用時(shí)有一些注意事項(xiàng),可以參考官方的DesignGuildLines,這里簡(jiǎn)單說(shuō)一下。
當(dāng)一個(gè)signal被一個(gè)subscriber subscribe后,這個(gè)subscriber何時(shí)會(huì)被移除?答案是當(dāng)subscriber被sendComplete或sendError時(shí),或者手動(dòng)調(diào)用[disposable dispose]。
當(dāng)subscriber被dispose后,所有該subscriber相關(guān)的工作都會(huì)被停止或取消,如http請(qǐng)求,資源也會(huì)被釋放。
Signal events是線性的,不會(huì)出現(xiàn)并發(fā)的情況,除非顯示地指定Scheduler。所以-subscribeNext:error:completed:里的block不需要鎖定或者synchronized等操作,其他的events會(huì)依次排隊(duì),直到block處理完成。
Errors有優(yōu)先權(quán),如果有多個(gè)signals被同時(shí)監(jiān)聽(tīng),只要其中一個(gè)signal sendError,那么error就會(huì)立刻被傳送給subscriber,并導(dǎo)致signals終止執(zhí)行。相當(dāng)于Exception。
生成Signal時(shí),最好指定Name,-setNameWithFormat:方便調(diào)試。
block代碼中不要阻塞。
小結(jié)
盡管洋洋灑灑寫(xiě)了這么多,也只是對(duì)RAC有了個(gè)大概的了解,如果要更深入地了解RAC還是需要多讀文檔、代碼和相關(guān)項(xiàng)目。
RAC學(xué)習(xí)起來(lái)稍顯吃力,且相關(guān)的文章目前還不多,中文的就更少了,希望這篇文章能帶給你些幫助。
以下是我覺(jué)得還不錯(cuò)的RAC相關(guān)資源
FunctionalReactivePixels作者同時(shí)還出了一本FRP相關(guān)的書(shū),個(gè)人覺(jué)得看源碼就足夠了。
GroceryListRAC的作者之一 jspahrsummers 的一個(gè)項(xiàng)目
ReactiveCocoa Essentilas: Understanding and Using RACCommand介紹了RACCommand的使用,同時(shí)也涉及了RAC相關(guān)的一些點(diǎn)。
Transparent OAuth Token Refresh Using ReactiveCocoa這篇文章講了如何使用RAC來(lái)透明地獲取Access Token,然后繼續(xù)發(fā)送請(qǐng)求。
BNR: An Introduction to ReactiveCocoa(視頻)
注:該文章為轉(zhuǎn)載,原文地址為:limboy.me/tech/2013/12/27/reactivecocoa-2.html?