上一節(jié),我們很多次使用了ReactiveCocoa
的關(guān)鍵部分,這里有更多的機會來使用ReactiveCocoa
整個代碼庫。開始吧!
首先在我們的畫廊視圖控制器中實現(xiàn)三個不同的代理方法:CollectionViewDataSource
、CollectionViewDelegate
、高清圖視圖控制器的PhotoViewControllerDelegate
使用一個稱之為RACDelegateProxy
的實例,我們可以抽象委托類型的協(xié)議的任何方法實現(xiàn)(比如:那些返回void類型的)。
委托代理是一個稱為rac_signalForSelector:
對象的‘白板’,獲取當(dāng)Selector被調(diào)用時發(fā)送的新值的信號。
注意:你必須retain這個delegate對象,否則他們將會被釋放,你將會得到一個EXC_BAD_ACCESS
異常。添加下列私有屬性到畫廊視圖控制器:
@property (nonatomic, strong) id collectionViewDelegate;
同時你也需要導(dǎo)入RACDelegateProxy.h
,因為他不是ReactiveCocoa的核心部分,不包含在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塊。然而,我們有必要在視圖控制器實現(xiàn)里提供一個空存根方法以避免編譯器發(fā)出"實現(xiàn)不完全"之類的警告。
空存根方法:源于C++的一個非常不錯的函數(shù)設(shè)計方法。在設(shè)計整個程序時,一般會先編寫完所有的代碼,然后開始編譯和測試,但這樣有時候會出現(xiàn)一大堆錯誤而不知從哪里入手,這時我們可以采用空存根技術(shù)。
存根是一個僅僅返回某個意義不大的值的空函數(shù)。存根可以用來測試整個程序的邏輯關(guān)系,以及分塊實現(xiàn)程序的不同部分。
設(shè)計一個程序時,先分析設(shè)計程序的各個函數(shù)完成的功能;然后直接設(shè)計函數(shù)的存根并編譯,編譯通過,證明程序的邏輯關(guān)系沒有問題的情況下,再來分別實現(xiàn)各個不同的函數(shù)(存根)。
接下來,我們有更多的機會來抽象這個類中的方法。loadPopularPhotos
方法除了改變我們的狀態(tài)之外,并沒有什么卵用。如果ReactiveCocoa
能夠很好地監(jiān)控這些狀態(tài),讓我們不在這方面擔(dān)心的話,那肯定是極好的!幸運的是,我恰好知道這個~
我們移除這個方法,在viewDidLoad中鍵入下面的代碼來代碼這個方法的調(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];
}];
一開始我們只是進行了importPhotos
方法調(diào)用,不同的是,我們用signal
來存放其返回值。 然后,我們“捕抓”這個信號上的錯誤并將它打印出來(跟我們之前做的一樣,只不過語法不同而已)。比起subscribeError:
方法,catch:
方法處理的更為巧妙:它允許無錯誤值的信號穿透它,僅在信號有錯誤事件發(fā)生時才會調(diào)用它的block并發(fā)送其在發(fā)生錯誤時的返回值。這里我們使用catch:
方法,來過濾無錯誤的值。這個catch:
塊僅僅返回一個空信號。更多關(guān)于這方面知識的細(xì)節(jié)請參考StackOverFlow的問題。
上面的方式,有一點點污染了我們的局部變量作用域,這可以用下面的更簡潔的等效方法:
RAC(self, photosArray) = [[[[FRPPhotoImporter importPhotos]
doCompleted:^{
@strongify(self);
[self.collectionView reloadData];
}] logError] catchTo:[RACSignal empty]];
使用RAC宏,我們創(chuàng)建了photosLoaded
信號的最新值到photoArray
屬性的單向綁定。太好了,保持狀態(tài)!
我們來看一下,我們的collectionViewCell的子類實現(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
這里有兩個標(biāo)志性的點表明了一個使用ReactiveCocoa來抽象的機會。
subscription
屬性)RACDisposable
的生命周期無論何時調(diào)用一個RACDisposable
對象的dispose
方法,就是一個"這里有更加響應(yīng)式的方法來作某件事"的好信號。在我們的例子中,這種嗅覺是對的。
通過在FRPCell
創(chuàng)建一個新的屬性,我們能夠抽象掉使用prepareForReuse
方法的必要性。這個屬性就是photoModel
(我們之前的行為就像是一個只寫的屬性,現(xiàn)在它將變?yōu)榭勺x寫的了)。把屬性放在文件頂部:
@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)鍵路徑。這點微妙的區(qū)別,作用卻大大不同。當(dāng)self
的屬性photoModel
或者photoModel
的thumbnailData
屬性改變時,關(guān)鍵路徑photoModel.thumbnailData
將會收到一個被(這種變化所)引發(fā)的KVO消息。
現(xiàn)在我們總算徹底擺脫了subscription
屬性!
更多建議: