概述
為什么要使用RAC? 一個(gè)怪怪的東西,從Demo看也沒(méi)有讓代碼變得更好、更短,相反還造成理解上的困難,真的有必要去學(xué)它么?相信這是大多數(shù)人在接觸RAC時(shí)的想法。RAC不是單一功能的模塊,它是一個(gè)Framework,提供了一整套解決方案。其核心思想是「響應(yīng)數(shù)據(jù)的變化」,在這個(gè)基礎(chǔ)上有了Signal的概念,進(jìn)而可以幫助減少狀態(tài)變量(可以參考jspahrsummers的
PPT),使用MVVM架構(gòu),統(tǒng)一的異步編程模型等等。 為什么RAC更加適合編寫Cocoa App?說(shuō)這個(gè)之前,我們先來(lái)看下Web前端編程,因?yàn)橛行┫嗨浦帯D壳昂芑鸬腁ngularJS有一個(gè)很重要的特性:數(shù)據(jù)與視圖綁定。就是當(dāng)數(shù)據(jù)變化時(shí),視圖不需要額外的處理,便可正確地呈現(xiàn)最新的數(shù)據(jù)。而這也是RAC的亮點(diǎn)之一。RAC與Cocoa的編程模式,有點(diǎn)像AngularJS和jQuery。所以要了解RAC,需要先在觀念上做調(diào)整。 以下面這個(gè)Cell為例 正常的寫法可能是這樣,很直觀。
-?(void)configureWithItem:(HBItem?*)item?{?????self.username.text?=?item.text;?????[self.avatarImageView?setImageWithURL:?item.avatarURL];?????//?其他的一些設(shè)置?}? 但如果用RAC,可能就是這樣
-?(id)init?{?????if?(self?=?[super?init])?{?????????@weakify(self);?????????[RACObserve(self,?viewModel)?subscribeNext:^(HBItemViewModel?*viewModel)?{?????????????@strongify(self);?????????????self.username.text?=?viewModel.item.text;?????????????[self.avatarImageView?setImageWithURL:?viewModel.item.avatarURL];?????????????//?其他的一些設(shè)置?????????}];?????}?}? 也就是先把數(shù)據(jù)綁定,接下來(lái)只要數(shù)據(jù)有變化,就會(huì)自動(dòng)響應(yīng)變化。在這里,每次viewModel改變時(shí),內(nèi)容就會(huì)自動(dòng)變成該viewModel的內(nèi)容。
Signal Signal是RAC的核心,為了幫助理解,畫了這張簡(jiǎn)化圖 這里的數(shù)據(jù)源和sendXXX,可以理解為函數(shù)的參數(shù)和返回值。當(dāng)Signal處理完數(shù)據(jù)后,可以向下一個(gè)Signal或Subscriber傳送數(shù)據(jù)。可以看到上半部分的兩個(gè)Signal是冷的(cold),相當(dāng)于實(shí)現(xiàn)了某個(gè)函數(shù),但該函數(shù)沒(méi)有被調(diào)用。同時(shí)也說(shuō)明了Signal可以被組合使用,比如RACSignal *signalB = [signalA map:^id(id x){return x}],或RACSignal *signalB = [signalA take:1]等等。 當(dāng)signal被subscribe時(shí),就會(huì)處于熱(hot)的狀態(tài),也就是該函數(shù)會(huì)被執(zhí)行。比如上面的第二張圖,首先signalA可能發(fā)了一個(gè)網(wǎng)絡(luò)請(qǐng)求,拿到結(jié)果后,把數(shù)據(jù)通過(guò)sendNext方法傳遞到下一個(gè)signal,signalB可以根據(jù)需要做進(jìn)一步處理,比如轉(zhuǎn)換成相應(yīng)的Model,轉(zhuǎn)換完后再sendNext到subscriber,subscriber拿到數(shù)據(jù)后,再改變ViewModel,同時(shí)因?yàn)閂iew已經(jīng)綁定了ViewModel,所以拿到的數(shù)據(jù)會(huì)自動(dòng)在View里呈現(xiàn)。 還有,一個(gè)signal可以被多個(gè)subscriber訂閱,這里怕顯得太亂就沒(méi)有畫出來(lái),但每次被新的subscriber訂閱時(shí),都會(huì)導(dǎo)致數(shù)據(jù)源的處理邏輯被觸發(fā)一次,這很有可能導(dǎo)致意想不到的結(jié)果,需要注意一下。 當(dāng)數(shù)據(jù)從signal傳送到subscriber時(shí),還可以通過(guò)doXXX來(lái)做點(diǎn)事情,比如打印數(shù)據(jù)。 通過(guò)這張圖可以看到,這非常像中學(xué)時(shí)學(xué)的函數(shù),比如 f(x) = y,某一個(gè)函數(shù)的輸出又可以作為另一個(gè)函數(shù)的輸入,比如 f(f(x)) = z,這也正是「函數(shù)響應(yīng)式編程」(FRP)的核心。 有些地方需要注意下,比如把signal作為local變量時(shí),如果沒(méi)有被subscribe,那么方法執(zhí)行完后,該變量會(huì)被dealloc。但如果signal有被subscribe,那么subscriber會(huì)持有該signal,直到signal sendCompleted或sendError時(shí),才會(huì)解除持有關(guān)系,signal才會(huì)被dealloc。
RACCommand RACCommand是RAC很重要的組成部分,可以節(jié)省很多時(shí)間并且讓你的App變得更Robust,這篇文章可以幫助你更深入的理解,這里簡(jiǎn)單做一下介紹。 RACCommand 通常用來(lái)表示某個(gè)Action的執(zhí)行,比如點(diǎn)擊Button。它有幾個(gè)比較重要的屬性:executionSignals / errors / executing。 1、executionSignals是signal of signals,如果直接subscribe的話會(huì)得到一個(gè)signal,而不是我們想要的value,所以一般會(huì)配合switchToLatest。 2、errors。跟正常的signal不一樣,RACCommand的錯(cuò)誤不是通過(guò)sendError來(lái)實(shí)現(xiàn)的,而是通過(guò)errors屬性傳遞出來(lái)的。 3、executing表示該command當(dāng)前是否正在執(zhí)行。 假設(shè)有這么個(gè)需求:當(dāng)圖片載入完后,分享按鈕才可用。那么可以這樣:
RACSignal?*imageAvailableSignal?=?[RACObserve(self,?imageView.image)?map:id^(id?x){return?x???@YES?:?@NO}];?self.shareButton.rac_command?=?[[RACCommand?alloc]?initWithEnabled:imageAvailableSignal?signalBlock:^RACSignal?*(id?input)?{?????//?do?share?logic?}];? 除了與UIControl綁定之外,也可以手動(dòng)執(zhí)行某個(gè)command,比如雙擊圖片點(diǎn)贊,就可以這么實(shí)現(xiàn)。
//?ViewModel.m?-?(instancetype)init?{?????self?=?[super?init];?????if?(self)?{?????????void?(^updatePinLikeStatus)()?=?^{?????????????self.pin.likedCount?=?self.pin.hasLiked???self.pin.likedCount?-?1?:?self.pin.likedCount?+?1;?????????????self.pin.hasLiked?=?!self.pin.hasLiked;?????????};??????????????????_likeCommand?=?[[RACCommand?alloc]?initWithSignalBlock:^RACSignal?*(id?input)?{?????????????//?先展示效果,再發(fā)送請(qǐng)求?????????????updatePinLikeStatus();?????????????return?[[HBAPIManager?sharedManager]?likePinWithPinID:self.pin.pinID];?????????}];??????????????????[_likeCommand.errors?subscribeNext:^(id?x)?{?????????????//?發(fā)生錯(cuò)誤時(shí),回滾?????????????updatePinLikeStatus();?????????}];?????}?????return?self;?}??//?ViewController.m?-?(void)viewDidLoad?{?????[super?viewDidLoad];?????//?...?????@weakify(self);?????[RACObserve(self,?viewModel.hasLiked)?subscribeNex:^(id?x){?????????@strongify(self);?????????self.pinLikedCountLabel.text?=?self.viewModel.likedCount;?????????self.likePinImageView.image?=?[UIImage?imageNamed:self.viewModel.hasLiked???@"pin_liked"?:?@"pin_like"];?????}];??????????UITapGestureRecognizer?*tapGesture?=?[[UITapGestureRecognizer?alloc]?init];?????tapGesture.numberOfTapsRequired?=?2;?????[[tapGesture?rac_gestureSignal]?subscribeNext:^(id?x)?{?????????[self.viewModel.likeCommand?execute:nil];?????}];?}? 再比如某個(gè)App要通過(guò)Twitter登錄,同時(shí)允許取消登錄,就可以這么做 (source)
_twitterLoginCommand?=?[[RACCommand?alloc]?initWithSignalBlock:^(id?_)?{???????@strongify(self);???????return?[[self????????????twitterSignInSignal]????????????takeUntil:self.cancelCommand.executionSignals];?????}];??RAC(self.authenticatedUser)?=?[self.twitterLoginCommand.executionSignals?switchToLatest];? 常用的模式 map + switchToLatest switchToLatest: 的作用是自動(dòng)切換signal of signals到最后一個(gè),比如之前的command.executionSignals就可以使用switchToLatest:。 map:的作用很簡(jiǎn)單,對(duì)sendNext的value做一下處理,返回一個(gè)新的值。 如果把這兩個(gè)結(jié)合起來(lái)就有意思了,想象這么個(gè)場(chǎng)景,當(dāng)用戶在搜索框輸入文字時(shí),需要通過(guò)網(wǎng)絡(luò)請(qǐng)求返回相應(yīng)的hints,每當(dāng)文字有變動(dòng)時(shí),需要取消上一次的請(qǐng)求,就可以使用這個(gè)配搭。這里用另一個(gè)Demo,簡(jiǎn)單演示一下
NSArray?*pins?=?@[@172230988,?@172230947,?@172230899,?@172230777,?@172230707];?__block?NSInteger?index?=?0;??RACSignal?*signal?=?[[[[RACSignal?interval:0.1?onScheduler:[RACScheduler?scheduler]]?????????????????????????take:pins.count]?????????????????????????map:^id(id?value)?{?????????????????????????????return?[[[HBAPIManager?sharedManager]?fetchPinWithPinID:[pins[index++]?intValue]]?doNext:^(id?x)?{?????????????????????????????????NSLog(@"這里只會(huì)執(zhí)行一次");?????????????????????????????}];?????????????????????????}]?????????????????????????switchToLatest];??[signal?subscribeNext:^(HBPin?*pin)?{?????NSLog(@"pinID:%d",?pin.pinID);?}?completed:^{?????NSLog(@"completed");?}];??//?output?//?2014-06-05?17:40:49.851?這里只會(huì)執(zhí)行一次?//?2014-06-05?17:40:49.851?pinID:172230707?//?2014-06-05?17:40:49.851?completed? takeUntil takeUntil:someSignal 的作用是當(dāng)someSignal sendNext時(shí),當(dāng)前的signal就sendCompleted,someSignal就像一個(gè)拳擊裁判,哨聲響起就意味著比賽終止。 它的常用場(chǎng)景之一是處理cell的button的點(diǎn)擊事件,比如點(diǎn)擊Cell的詳情按鈕,需要push一個(gè)VC,就可以這樣:
[[[cell.detailButton?????rac_signalForControlEvents:UIControlEventTouchUpInside]?????takeUntil:cell.rac_prepareForReuseSignal]?????subscribeNext:^(id?x)?{?????????//?generate?and?push?ViewController?}];? 如果不加takeUntil:cell.rac_prepareForReuseSignal,那么每次Cell被重用時(shí),該button都會(huì)被addTarget:selector。
替換Delegate 出現(xiàn)這種需求,通常是因?yàn)樾枰獙?duì)Delegate的多個(gè)方法做統(tǒng)一的處理,這時(shí)就可以造一個(gè)signal出來(lái),每次該Delegate的某些方法被觸發(fā)時(shí),該signal就會(huì)sendNext。
@implementation?UISearchDisplayController?(RAC)?-?(RACSignal?*)rac_isActiveSignal?{?????self.delegate?=?self;?????RACSignal?*signal?=?objc_getAssociatedObject(self,?_cmd);?????if?(signal?!=?nil)?return?signal;??????????/*?Create?two?signals?and?merge?them?*/?????RACSignal?*didBeginEditing?=?[[self?rac_signalForSelector:@selector(searchDisplayControllerDidBeginSearch:)??????????????????????????????????????????fromProtocol:@protocol(UISearchDisplayDelegate)]?mapReplace:@YES];?????RACSignal?*didEndEditing?=?[[self?rac_signalForSelector:@selector(searchDisplayControllerDidEndSearch:)????????????????????????????????????????fromProtocol:@protocol(UISearchDisplayDelegate)]?mapReplace:@NO];?????signal?=?[RACSignal?merge:@[didBeginEditing,?didEndEditing]];???????????????objc_setAssociatedObject(self,?_cmd,?signal,?OBJC_ASSOCIATION_RETAIN_NONATOMIC);?????return?signal;?}?@end? 代碼源于
此文 使用ReactiveViewModel的didBecomActiveSignal ReactiveViewModel是另一個(gè)project, 后面的MVVM中會(huì)講到,通常的做法是在VC里設(shè)置VM的active屬性(RVMViewModel自帶該屬性),然后在VM里subscribeNext didBecomActiveSignal,比如當(dāng)Active時(shí),獲取TableView的最新數(shù)據(jù)。
RACSubject的使用場(chǎng)景 一般不推薦使用RACSubject,因?yàn)樗^(guò)于靈活,濫用的話容易導(dǎo)致復(fù)雜度的增加。但有一些場(chǎng)景用一下還是比較方便的,比如ViewModel的errors。 ViewModel一般會(huì)有多個(gè)RACCommand,那這些commands如果出現(xiàn)error了該如何處理呢?比較方便的方法如下:
//?HBCViewModel.h??#import?"RVMViewModel.h"??@class?RACSubject;??@interface?HBCViewModel?:?RVMViewModel?@property?(nonatomic)?RACSubject?*errors;?@end????//?HBCViewModel.m??#import?"HBCViewModel.h"?#import?<ReactiveCocoa.h>??@implementation?HBCViewModel??-?(instancetype)init?{?????self?=?[super?init];?????if?(self)?{?????????_errors?=?[RACSubject?subject];?????}?????return?self;?}??-?(void)dealloc?{?????[_errors?sendCompleted];?}?@end??//?Some?Other?ViewModel?inherit?HBCViewModel??-?(instancetype)init?{?????_fetchLatestCommand?=?[RACCommand?alloc]?initWithSignalBlock:^RACSignal?*(id?input){?????????//?fetch?latest?data?????}];??????_fetchMoreCommand?=?[RACCommand?alloc]?initWithSignalBlock:^RACSignal?*(id?input){?????????//?fetch?more?data?????}];??????[self.didBecomeActiveSignal?subscribeNext:^(id?x)?{?????????[_fetchLatestCommand?execute:nil];?????}];??????????[[RACSignal?????????merge:@[?????????????????_fetchMoreCommand.errors,?????????????????_fetchLatestCommand.errors?????????????????]]?subscribe:self.errors];??}? rac_signalForSelector rac_signalForSelector: 這個(gè)方法會(huì)返回一個(gè)signal,當(dāng)selector執(zhí)行完時(shí),會(huì)sendNext,也就是當(dāng)某個(gè)方法調(diào)用完后再額外做一些事情。用在category會(huì)比較方便,因?yàn)镃ategory重寫父類的方法時(shí),不能再通過(guò)[super XXX]來(lái)調(diào)用父類的方法,當(dāng)然也可以手寫Swizzle來(lái)實(shí)現(xiàn),不過(guò)有了rac_signalForSelector:就方便多了。 rac_signalForSelector: fromProtocol: 可以直接實(shí)現(xiàn)對(duì)protocol的某個(gè)方法的實(shí)現(xiàn)(聽(tīng)著有點(diǎn)別扭呢),比如,我們想實(shí)現(xiàn)UIScrollViewDelegate的某些方法,可以這么寫
[[self?rac_signalForSelector:@selector(scrollViewDidEndDecelerating:)?fromProtocol:@protocol(UIScrollViewDelegate)]?subscribeNext:^(RACTuple?*tuple)?{?????//?do?something?}];??[[self?rac_signalForSelector:@selector(scrollViewDidScroll:)?fromProtocol:@protocol(UIScrollViewDelegate)]?subscribeNext:^(RACTuple?*tuple)?{?????//?do?something?}];??self.scrollView.delegate?=?nil;?self.scrollView.delegate?=?self;? 注意,這里的delegate需要先設(shè)置為nil,再設(shè)置為self,而不能直接設(shè)置為self,如果self已經(jīng)是該scrollView的Delegate的話。 有時(shí),我們想對(duì)selector的返回值做一些處理,但很遺憾RAC不支持,如果真的有需要的話,可以使用
Aspects MVVM 這是一個(gè)大話題,如果有耐心,且英文還不錯(cuò)的話,可以看一下Cocoa Samurai的這
兩篇文章。PS: Facebook Paper就是基于MVVM構(gòu)建的。 MVVM是Model-View-ViewModel的簡(jiǎn)稱,它們之間的關(guān)系如下 可以看到View(其實(shí)是ViewController)持有ViewModel,這樣做的好處是ViewModel更加獨(dú)立且可測(cè)試,ViewModel里不應(yīng)包含任何View相關(guān)的元素,哪怕?lián)Q了一個(gè)View也能正常工作。而且這樣也能讓View/ViewController「瘦」下來(lái)。 ViewModel主要做的事情是作為View的數(shù)據(jù)源,所以通常會(huì)包含網(wǎng)絡(luò)請(qǐng)求。 或許你會(huì)疑惑,ViewController哪去了?在MVVM的世界里,ViewController已經(jīng)成為了View的一部分。它的主要職責(zé)是將VM與View綁定、響應(yīng)VM數(shù)據(jù)的變化、調(diào)用VM的某個(gè)方法、與其他的VC打交道。 而RAC為MVVM帶來(lái)很大的便利,比如RACCommand, UIKit的RAC Extension等等。使用MVVM不一定能減少代碼量,但能降低代碼的復(fù)雜度。 以下面這個(gè)需求為例,要求大圖滑動(dòng)結(jié)束時(shí),底部的縮略圖滾動(dòng)到對(duì)應(yīng)的位置,并高亮該縮略圖;同時(shí)底部的縮略圖被選中時(shí),大圖也要變成該縮略圖的大圖。 我的思路是橫向滾動(dòng)的大圖是一個(gè)collectionView,該collectionView是當(dāng)前頁(yè)面VC的一個(gè)property。底部可以滑動(dòng)的縮略圖是一個(gè)childVC的collectionView,這兩個(gè)collectionView共用一套VM,并且各自RACObserve感興趣的property。 比如大圖滑到下一頁(yè)時(shí),會(huì)改變VM的indexPath屬性,而底部的collectionView所在的VC正好對(duì)該indexPath感興趣,只要indexPath變化就滾動(dòng)到相應(yīng)的Item
//?childVC??-?(void)viewDidLoad?{?????[super?viewDidLoad];??????@weakify(self);?????[RACObserve(self,?viewModel.indexPath)?subscribeNext:^(NSNumber?*index)?{?????????@strongify(self);?????????[self?scrollToIndexPath];?????}];?}??-?(void)scrollToIndexPath?{?????if?(self.collectionView.subviews.count)?{?????????NSIndexPath?*indexPath?=?self.viewModel.indexPath;?????????[self.collectionView?scrollToItemAtIndexPath:indexPath?atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally?animated:YES];?????????[self.collectionView.subviews?enumerateObjectsUsingBlock:^(UIView?*view,?NSUInteger?idx,?BOOL?*stop)?{?????????????view.layer.borderWidth?=?0;?????????}];?????????UIView?*view?=?[self.collectionView?cellForItemAtIndexPath:indexPath];?????????view.layer.borderWidth?=?kHBPinsNaviThumbnailPadding;?????????view.layer.borderColor?=?[UIColor?whiteColor].CGColor;?????}?}? 當(dāng)點(diǎn)擊底部的縮略圖時(shí),上面的大圖也要做出變化,也同樣可以通過(guò)RACObserve indexPath來(lái)實(shí)現(xiàn)
//?PinsViewController.m?-?(void)viewDidLoad?{?????[super?viewDidLoad];?????@weakify(self);?????[[RACObserve(self,?viewModel.indexPath)?????????skip:1]?????????subscribeNext:^(NSIndexPath?*indexPath)?{?????????????@strongify(self);?????????????[self.collectionView?scrollToItemAtIndexPath:indexPath?atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally?animated:YES];?????}];?}? 這里有一個(gè)小技巧,當(dāng)Cell里的元素比較復(fù)雜時(shí),我們可以給Cell也準(zhǔn)備一個(gè)ViewModel,這個(gè)CellViewModel可以由上一層的ViewModel提供,這樣Cell如果需要相應(yīng)的數(shù)據(jù),直接跟CellViewModel要即可,CellViewModel也可以包含一些command,比如likeCommand。假如點(diǎn)擊Cell時(shí),要做一些處理,也很方便。
//?CellViewModel已經(jīng)在ViewModel里準(zhǔn)備好了?-?(UICollectionViewCell?*)collectionView:(UICollectionView?*)collectionView?cellForItemAtIndexPath:(NSIndexPath?*)indexPath?{?????HBPinsCell?*cell?=?[collectionView?dequeueReusableCellWithReuseIdentifier:cellIdentifier?forIndexPath:indexPath];?????cell.viewModel?=?self.viewModel.cellViewModels[indexPath.row];?????return?cell;?}??-?(void)collectionView:(UICollectionView?*)collectionView?didSelectItemAtIndexPath:(NSIndexPath?*)indexPath?{?????HBCellViewModel?*cellViewModel?=?self.viewModel.cellViewModels[indexPath.row];?????//?對(duì)cellViewModel執(zhí)行某些操作,因?yàn)镃ell已經(jīng)與cellViewModel綁定,所以cellViewModel的改變也會(huì)反映到Cell上?????//?或拿到cellViewModel的數(shù)據(jù)來(lái)執(zhí)行某些操作?}? ViewModel中signal, property, command的使用 初次使用RAC+MVVM時(shí),往往會(huì)疑惑,什么時(shí)候用signal,什么時(shí)候用property,什么時(shí)候用command? 一般來(lái)說(shuō)可以使用property的就直接使用,沒(méi)必要再轉(zhuǎn)換成signal,外部RACObserve即可。使用signal的場(chǎng)景一般是涉及到多個(gè)property或多個(gè)signal合并為一個(gè)signal。command往往與UIControl/網(wǎng)絡(luò)請(qǐng)求掛鉤。
常見(jiàn)場(chǎng)景的處理 檢查本地緩存,如果失效則去請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)并緩存到本地 來(lái)源 -?(RACSignal?*)loadData?{?????return?[[RACSignal??????????createSignal:^(id<RACSubscriber>?subscriber)?{?????????????//?If?the?cache?is?valid?then?we?can?just?immediately?send?the??????????????//?cached?data?and?be?done.?????????????if?(self.cacheValid)?{?????????????????[subscriber?sendNext:self.cachedData];?????????????????[subscriber?sendCompleted];?????????????}?else?{?????????????????[subscriber?sendError:self.staleCacheError];?????????????}?????????}]??????????//?Do?the?subscription?work?on?some?random?scheduler,?off?the?main??????????//?thread.?????????subscribeOn:[RACScheduler?scheduler]];?}??-?(void)update?{?????[[[[self??????????loadData]?????????//?Catch?the?error?from?-loadData.?It?means?our?cache?is?stale.?Update?????????//?our?cache?and?save?it.?????????catch:^(NSError?*error)?{?????????????return?[[self?updateCachedData]?doNext:^(id?data)?{?????????????????[self?cacheData:data];?????????????}];?????????}]??????????//?Our?work?up?until?now?has?been?on?a?background?scheduler.?Get?our??????????//?results?delivered?on?the?main?thread?so?we?can?do?UI?work.?????????deliverOn:RACScheduler.mainThreadScheduler]?????????subscribeNext:^(id?data)?{?????????????//?Update?your?UI?based?on?`data`.??????????????//?Update?again?after?`updateInterval`?seconds?have?passed.?????????????[[RACSignal?interval:updateInterval]?take:1]?subscribeNext:^(id?_)?{?????????????????[self?update];?????????????}];?????????}];??}? 檢測(cè)用戶名是否可用 ? 來(lái)源 -?(void)setupUsernameAvailabilityChecking?{?????RAC(self,?availabilityStatus)?=?[[[RACObserve(self.userTemplate,?username)???????????????????????????????????????throttle:kUsernameCheckThrottleInterval]?//throttle表示interval時(shí)間內(nèi)如果有sendNext,則放棄該nextValue???????????????????????????????????????map:^(NSString?*username)?{???????????????????????????????????????????if?(username.length?==?0)?return?[RACSignal?return:@(UsernameAvailabilityCheckStatusEmpty)];???????????????????????????????????????????return?[[[[[FIBAPIClient?sharedInstance]?????????????????????????????????????????????????getUsernameAvailabilityFor:username?ignoreCache:NO]???????????????????????????????????????????????map:^(NSDictionary?*result)?{???????????????????????????????????????????????????NSNumber?*existsNumber?=?result[@"exists"];???????????????????????????????????????????????????if?(!existsNumber)?return?@(UsernameAvailabilityCheckStatusFailed);???????????????????????????????????????????????????UsernameAvailabilityCheckStatus?status?=?[existsNumber?boolValue]???UsernameAvailabilityCheckStatusUnavailable?:?UsernameAvailabilityCheckStatusAvailable;???????????????????????????????????????????????????return?@(status);???????????????????????????????????????????????}]??????????????????????????????????????????????catch:^(NSError?*error)?{???????????????????????????????????????????????????return?[RACSignal?return:@(UsernameAvailabilityCheckStatusFailed)];???????????????????????????????????????????????}]?startWith:@(UsernameAvailabilityCheckStatusChecking)];???????????????????????????????????????}]???????????????????????????????????????switchToLatest];?}? 可以看到這里也使用了map + switchToLatest模式,這樣就可以自動(dòng)取消上一次的網(wǎng)絡(luò)請(qǐng)求。 startWith的內(nèi)部實(shí)現(xiàn)是concat,這里表示先將狀態(tài)置為checking,然后再根據(jù)網(wǎng)絡(luò)請(qǐng)求的結(jié)果設(shè)置狀態(tài)。
使用takeUntil:來(lái)處理Cell的button點(diǎn)擊 這個(gè)上面已經(jīng)提到過(guò)了。
token過(guò)期后自動(dòng)獲取新的 開(kāi)發(fā)APIClient時(shí),會(huì)用到AccessToken,這個(gè)Token過(guò)一段時(shí)間會(huì)過(guò)期,需要去請(qǐng)求新的Token。比較好的用戶體驗(yàn)是當(dāng)token過(guò)期后,自動(dòng)去獲取新的Token,拿到后繼續(xù)上一次的請(qǐng)求,這樣對(duì)用戶是透明的。
RACSignal?*requestSignal?=?[RACSignal?createSignal:^RACDisposable?*(id<RACSubscriber>?subscriber)?{?????????//?suppose?first?time?send?request,?access?token?is?expired?or?invalid?????????//?and?next?time?it?is?correct.?????????//?the?block?will?be?triggered?twice.?????????static?BOOL?isFirstTime?=?0;?????????NSString?*url?=?@"http://httpbin.org/ip";?????????if?(!isFirstTime)?{?????????????url?=?@"http://nonexists.com/error";?????????????isFirstTime?=?1;?????????}?????????NSLog(@"url:%@",?url);?????????[[AFHTTPRequestOperationManager?manager]?GET:url?parameters:nil?success:^(AFHTTPRequestOperation?*operation,?id?responseObject)?{?????????????[subscriber?sendNext:responseObject];?????????????[subscriber?sendCompleted];?????????}?failure:^(AFHTTPRequestOperation?*operation,?NSError?*error)?{?????????????[subscriber?sendError:error];?????????}];?????????return?nil;?????}];??????????self.statusLabel.text?=?@"sending?request...";?????[[requestSignal?catch:^RACSignal?*(NSError?*error)?{?????????self.statusLabel.text?=?@"oops,?invalid?access?token";??????????????????//?simulate?network?request,?and?we?fetch?the?right?access?token?????????return?[[RACSignal?createSignal:^RACDisposable?*(id<RACSubscriber>?subscriber)?{?????????????double?delayInSeconds?=?1.0;?????????????dispatch_time_t?popTime?=?dispatch_time(DISPATCH_TIME_NOW,?(int64_t)(delayInSeconds?*?NSEC_PER_SEC));?????????????dispatch_after(popTime,?dispatch_get_main_queue(),?^(void){?????????????????[subscriber?sendNext:@YES];?????????????????[subscriber?sendCompleted];?????????????});?????????????return?nil;?????????}]?concat:requestSignal];?????}]?subscribeNext:^(id?x)?{?????????if?([x?isKindOfClass:[NSDictionary?class]])?{?????????????self.statusLabel.text?=?[NSString?stringWithFormat:@"result:%@",?x[@"origin"]];?????????}?????}?completed:^{?????????NSLog(@"completed");?????}];? 注意事項(xiàng) RAC我自己感覺(jué)遇到的幾個(gè)難點(diǎn)是: 1) 理解RAC的理念。 2) 熟悉常用的API。3) 針對(duì)某些特定的場(chǎng)景,想出比較合理的RAC處理方式。不過(guò)看多了,寫多了,想多了就會(huì)慢慢適應(yīng)。下面是我在實(shí)踐過(guò)程中遇到的一些小坑。
ReactiveCocoaLayout 有時(shí)Cell的內(nèi)容涉及到動(dòng)態(tài)的高度,就會(huì)想到用Autolayout來(lái)布局,但RAC已經(jīng)為我們準(zhǔn)備好了
ReactiveCocoaLayout,所以我想不妨就拿來(lái)用一下。 ReactiveCocoaLayout的使用好比「批地」和「蓋房」,先通過(guò)insetWidth:height:nullRect從某個(gè)View中劃出一小塊,拿到之后還可以通過(guò)divideWithAmount:padding:fromEdge 再分成兩塊,或sliceWithAmount:fromEdge再分出一塊。這些方法返回的都是signal,所以可以通過(guò)RAC(self.view, frame) = someRectSignal 這樣來(lái)實(shí)現(xiàn)綁定。但在實(shí)踐中發(fā)現(xiàn)性能不是很好,多批了幾塊地就容易造成主線程卡頓。 所以ReactiveCocoaLayout最好不用或少用。
調(diào)試 剛開(kāi)始寫RAC時(shí),往往會(huì)遇到這種情況,滿屏的調(diào)用棧信息都是RAC的,要找出真正出現(xiàn)問(wèn)題的地方不容易。曾經(jīng)有一次在使用[RACSignal combineLatest: reduce:^id{}]時(shí),忘了在Block里返回value,而Xcode也沒(méi)有提示warning,然后就是莫名其妙地掛起了,跳到了匯編上,也沒(méi)有調(diào)用棧信息,這時(shí)就只能通過(guò)最古老的注釋代碼的方式來(lái)找到問(wèn)題的根源。 不過(guò)寫多了之后,一般不太會(huì)犯這種低級(jí)錯(cuò)誤。
strongify / weakify dance 因?yàn)镽AC很多操作都是在Block中完成的,這塊最常見(jiàn)的問(wèn)題就是在block直接把self拿來(lái)用,造成block和self的retain cycle。所以需要通過(guò)@strongify和@weakify來(lái)消除循環(huán)引用。 有些地方很容易被忽略,比如RACObserve(thing, keypath),看上去并沒(méi)有引用self,所以在subscribeNext時(shí)就忘記了weakify/strongify。但事實(shí)上RACObserve總是會(huì)引用self,即使target不是self,所以只要有RACObserve的地方都要使用weakify/strongify。
轉(zhuǎn)自:http://www.cocoachina.com/industry/20140609/8737.html
總結(jié)
以上是生活随笔為你收集整理的ReactiveCocoa入门的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。