上一節(jié),我們很多次使用了ReactiveCocoa
的關(guān)鍵部分,這里有更多的機(jī)會(huì)來(lái)使用ReactiveCocoa
整個(gè)代碼庫(kù)。開(kāi)始吧!
首先在我們的畫(huà)廊視圖控制器中實(shí)現(xiàn)三個(gè)不同的代理方法:CollectionViewDataSource
、CollectionViewDelegate
、高清圖視圖控制器的PhotoViewControllerDelegate
使用一個(gè)稱之為RACDelegateProxy
的實(shí)例,我們可以抽象委托類型的協(xié)議的任何方法實(shí)現(xiàn)(比如:那些返回void類型的)。
委托代理是一個(gè)稱為rac_signalForSelector:
對(duì)象的‘白板’,獲取當(dāng)Selector被調(diào)用時(shí)發(fā)送的新值的信號(hào)。
注意:你必須retain這個(gè)delegate對(duì)象,否則他們將會(huì)被釋放,你將會(huì)得到一個(gè)EXC_BAD_ACCESS
異常。添加下列私有屬性到畫(huà)廊視圖控制器:
@property (nonatomic, strong) id collectionViewDelegate;
同時(shí)你也需要導(dǎo)入RACDelegateProxy.h
,因?yàn)樗皇荝eactiveCocoa的核心部分,不包含在ReactiveCocoa.h
中。移除UICollectionViewDelegate
以及FRPFullsizePhotoViewControllerDelegate
方法,追加下面的代碼到viewDidLoad
.
RACDelegateProxy *viewControllerDelegate = [[RACDelegateProxy alloc]
initWithProtocol:@protocol(FRPFullSizePhotoViewControllerDelegate)];
[[viewControllerDelegate rac_signalForSelector:@selector(userDidScroll:toPhotoAtIndex:) fromProtocol:@protocol(FRPFullSizePhotoViewControllerDelegate)]
subscribeNext:^(RACTuple *value){
@strongify(self);
[self.collectionView
scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:[value.second integerValue] inSection:0]
atScrollPosition:UICollectionViewScrollPositionCenteredVertically
animated:NO];
}];
self.collectionViewDelegate = [[RACDelegateProxy alloc] initWithProtocol:@protocol(UICollectionViewDelegate)];
[[self.collectionViewDelegate rac_signalForSelector:@selector(collectionView:didSelectItemAtIndexPath:)]
subscribeNext:^(RACTuple *arguments) {
@strongify(self);
FRPFullSizePhotoViewController *viewController = [[FRPFullSizePhotoViewController alloc] initWithPhotoModels:self.photosArray currentPhotoIndex:[(NSIndexPath *)arguments.second item]];
viewController.delegate = (id<FRPFullSizePhotoViewControllerDelegate>)viewControllerDelegate;
[self.navigationController pushViewController:viewController animated:YES];
}];
我們也可以在self
上調(diào)用rac_signalForSelector:
,使用同樣的block塊。然而,我們有必要在視圖控制器實(shí)現(xiàn)里提供一個(gè)空存根方法以避免編譯器發(fā)出"實(shí)現(xiàn)不完全"之類的警告。
空存根方法:源于C++的一個(gè)非常不錯(cuò)的函數(shù)設(shè)計(jì)方法。在設(shè)計(jì)整個(gè)程序時(shí),一般會(huì)先編寫(xiě)完所有的代碼,然后開(kāi)始編譯和測(cè)試,但這樣有時(shí)候會(huì)出現(xiàn)一大堆錯(cuò)誤而不知從哪里入手,這時(shí)我們可以采用空存根技術(shù)。
存根是一個(gè)僅僅返回某個(gè)意義不大的值的空函數(shù)。存根可以用來(lái)測(cè)試整個(gè)程序的邏輯關(guān)系,以及分塊實(shí)現(xiàn)程序的不同部分。
設(shè)計(jì)一個(gè)程序時(shí),先分析設(shè)計(jì)程序的各個(gè)函數(shù)完成的功能;然后直接設(shè)計(jì)函數(shù)的存根并編譯,編譯通過(guò),證明程序的邏輯關(guān)系沒(méi)有問(wèn)題的情況下,再來(lái)分別實(shí)現(xiàn)各個(gè)不同的函數(shù)(存根)。
接下來(lái),我們有更多的機(jī)會(huì)來(lái)抽象這個(gè)類中的方法。loadPopularPhotos
方法除了改變我們的狀態(tài)之外,并沒(méi)有什么卵用。如果ReactiveCocoa
能夠很好地監(jiān)控這些狀態(tài),讓我們不在這方面擔(dān)心的話,那肯定是極好的!幸運(yùn)的是,我恰好知道這個(gè)~
我們移除這個(gè)方法,在viewDidLoad中鍵入下面的代碼來(lái)代碼這個(gè)方法的調(diào)用:
RACSignal *photoSignal = [FRPPhotoImporter importPhotos];
RACSignal *photosLoaded = [photoSignal catch:^RACSignal *(NSError *error) {
NSLog(@"Couldn't fetch photos from 500px : %@",error);
return [RACSignal empty];
}];
RAC(self, photosArray) = photosLoaded;
[photosLoaded subscribeCompleted: ^{
@strongify(self);
[self.conllectionView reloadData];
}];
一開(kāi)始我們只是進(jìn)行了importPhotos
方法調(diào)用,不同的是,我們用signal
來(lái)存放其返回值。 然后,我們“捕抓”這個(gè)信號(hào)上的錯(cuò)誤并將它打印出來(lái)(跟我們之前做的一樣,只不過(guò)語(yǔ)法不同而已)。比起subscribeError:
方法,catch:
方法處理的更為巧妙:它允許無(wú)錯(cuò)誤值的信號(hào)穿透它,僅在信號(hào)有錯(cuò)誤事件發(fā)生時(shí)才會(huì)調(diào)用它的block并發(fā)送其在發(fā)生錯(cuò)誤時(shí)的返回值。這里我們使用catch:
方法,來(lái)過(guò)濾無(wú)錯(cuò)誤的值。這個(gè)catch:
塊僅僅返回一個(gè)空信號(hào)。更多關(guān)于這方面知識(shí)的細(xì)節(jié)請(qǐng)參考StackOverFlow的問(wèn)題。
上面的方式,有一點(diǎn)點(diǎn)污染了我們的局部變量作用域,這可以用下面的更簡(jiǎn)潔的等效方法:
RAC(self, photosArray) = [[[[FRPPhotoImporter importPhotos]
doCompleted:^{
@strongify(self);
[self.collectionView reloadData];
}] logError] catchTo:[RACSignal empty]];
使用RAC宏,我們創(chuàng)建了photosLoaded
信號(hào)的最新值到photoArray
屬性的單向綁定。太好了,保持狀態(tài)!
我們來(lái)看一下,我們的collectionViewCell的子類實(shí)現(xiàn):
@interface FRPCell ()
@property (nonatomic, weak) UIImageView *imageView;
@property (nonatomic, strong) RACDisposable *subscription;
@end
@implementation FRPCell
- (instancetype)initWithFrame:(CGRect)frame {
...
}
- (void)perpareForReuse {
[super perpareForReuse];
[self.subscription dispose], self.subscription = nil;
}
- (void)setPhotoModel:(FRPPhotoModel *)photoModel {
self.subscription = [[[RACObserve(photoModel, thumbnailData) filter:^BOOL(id value) {
return value != nil;
}] map:^id(id value) {
return [UIImage imageWithData:value];
}] setKeyPath:@keypath(self.imageView, image) onObject:self.imageView];
}
@end
這里有兩個(gè)標(biāo)志性的點(diǎn)表明了一個(gè)使用ReactiveCocoa來(lái)抽象的機(jī)會(huì)。
subscription
屬性)RACDisposable
的生命周期無(wú)論何時(shí)調(diào)用一個(gè)RACDisposable
對(duì)象的dispose
方法,就是一個(gè)"這里有更加響應(yīng)式的方法來(lái)作某件事"的好信號(hào)。在我們的例子中,這種嗅覺(jué)是對(duì)的。
通過(guò)在FRPCell
創(chuàng)建一個(gè)新的屬性,我們能夠抽象掉使用prepareForReuse
方法的必要性。這個(gè)屬性就是photoModel
(我們之前的行為就像是一個(gè)只寫(xiě)的屬性,現(xiàn)在它將變?yōu)榭勺x寫(xiě)的了)。把屬性放在文件頂部:
@property (nonatomic, strong ) FRPPhotoModel *photoModel;
下一步我們將徹底擺脫setPhotoModel:
方法。我們將為photoModel的thumbnailData
觀察我們自己的關(guān)鍵路徑。將下面的代碼添加到cell的初始化函數(shù)中。
RAC(self.imageView, image) = [[RACObserve(self, photoModel.thumbnailData) ignore:nil]
map:^(NSData *data){
return [UIImage imageWithData:data];
}];
注意看我們觀察的是self
的photoModel.thumbnailData
的關(guān)鍵路徑,而非self.photoModel
的thumbnailData
的關(guān)鍵路徑。這點(diǎn)微妙的區(qū)別,作用卻大大不同。當(dāng)self
的屬性photoModel
或者photoModel
的thumbnailData
屬性改變時(shí),關(guān)鍵路徑photoModel.thumbnailData
將會(huì)收到一個(gè)被(這種變化所)引發(fā)的KVO消息。
現(xiàn)在我們總算徹底擺脫了subscription
屬性!
更多建議: