和FunctionalReactivePixels一起實(shí)踐

2018-08-01 15:49 更新

上一節(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ì)。

  1. 我們有狀態(tài)(subscription屬性)
  2. 我們手動(dòng)處理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];
                            }];

注意看我們觀察的是selfphotoModel.thumbnailData的關(guān)鍵路徑,而非self.photoModelthumbnailData的關(guān)鍵路徑。這點(diǎn)微妙的區(qū)別,作用卻大大不同。當(dāng)self的屬性photoModel或者photoModelthumbnailData屬性改變時(shí),關(guān)鍵路徑photoModel.thumbnailData將會(huì)收到一個(gè)被(這種變化所)引發(fā)的KVO消息。

現(xiàn)在我們總算徹底擺脫了subscription屬性!


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)