FunctionalReactivePixels基礎(chǔ)

2018-08-01 16:17 更新

FunctionReactivePixels將會(huì)是一個(gè)簡(jiǎn)單的觀看'500px'中最受歡迎的照片的應(yīng)用。一旦我們完成這一節(jié),應(yīng)用的主界面將會(huì)像下面這樣:

app_main_page

當(dāng)然我們也可以像下圖一樣觀看全屏模式下的圖片。

ReactiveCocoa實(shí)踐之FunctionalReactivePixels基礎(chǔ)

這個(gè)App將使用Collection Views。如果你沒(méi)有太多這方面的經(jīng)驗(yàn),也不需要太過(guò)擔(dān)心---他們(CollectionView)就像TableView一樣,使用起來(lái)非常簡(jiǎn)單。如果你對(duì)UICollectionView感興趣,可以閱讀我的另一本書(shū).

我們將使用CocoaPods來(lái)管理我們的依賴,現(xiàn)在創(chuàng)建一個(gè)新的工程。我喜歡使用空模版以便我可以完全控制viewController層級(jí)。

ReactiveCocoa實(shí)踐之FunctionalReactivePixels基礎(chǔ)

首先、我們將創(chuàng)建一個(gè)UICollectionViewController的子類FRPGalleryViewController.同時(shí)我們創(chuàng)建一個(gè)UICollectionViewFlowLayout的子類FRPGalleryFlowLayout.

#import the new flow layout's header in the view controller's implementation file and
#then override FRPGalleryViewController's init method

- (id)init{
    FRPGalleryFlowLayout *flowLayout = [[FRPGalleryFlowLayout alloc] init];
    self = [self initWithCollectionViewLayout:flowLayout];
    if(!self) return nil;
    return self;
}

這將初始化collection View的layout為我們自己的layout.這個(gè)flowlayout子類的實(shí)現(xiàn)非常簡(jiǎn)單,只需要設(shè)置一些屬性就可以了。

@implementation FRPGalleryFlowLayout
- (instancetype)init{
    if (!(self = [super init])) return nil;

    self.itemSize = CGSizeMake(145,145);
    self.minimumInteritemSpacing = 10;
    self.minimumLineSpacing = 10;
    self.sectionInset = UIEdgeInsetsMake(10,10,10,10);

    return self;
}
@end

很棒!下一步,我們需要把Viewcontroller展現(xiàn)在屏幕上。為了實(shí)現(xiàn)這個(gè),我們首先要在應(yīng)用的application delegate的application: didFinishLaunchingWithOptions:方法。我們想要將collectionview Controller置于一個(gè)navigationController容器中:

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[[FRPGalleryViewController alloc] init]];

    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}

很好!如果我們現(xiàn)在運(yùn)行,我們將看到一個(gè)空視圖。

app_main_emptypage

我們來(lái)填充一些內(nèi)容。創(chuàng)建一個(gè)Podfile文件,并填寫(xiě)如下內(nèi)容:

platform :ios, "7.0"
target "FRP" do
    pod 'ReactiveCocoa', '~> 2.1.4'
    pod 'libextobjc', '~> 0.3'
    pod '500-iOS-api', '~> 1.0.4'
    pod 'SVProgressHUD', '~> 0.9'
end

target "FRPTests" do

end

下一章,我們將添加一些測(cè)試?,F(xiàn)在運(yùn)行pod install,然后打開(kāi)Xcode通用的workspace文件。打開(kāi)與編譯頭文件FRP-Prefix.pch(Xcode6之后,新建工程默認(rèn)不加載pch文件,需要自己添加,Apple的最佳實(shí)踐中已經(jīng)不推薦使用全局的預(yù)編譯pch文件),然后添加下面的內(nèi)容。這些語(yǔ)義會(huì)自動(dòng)加載到項(xiàng)目的所有文件中。

//Pods
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <500px-iOS-api/PXAPI.h>
#import <libextobjc/EXTScope.h>

//App Delegate
#import "FRPAppDelegate.h"
#define AppDelegate ((FRPAppDelegate *)[[UIApplication sharedApplication] delegate])

對(duì)于這樣使用AppDelegate單例的用法,Saul Mora說(shuō):“每次看到你這么做,我家的狗都想死”。 但是這不是一本關(guān)于設(shè)計(jì)模式的書(shū)---這是一本關(guān)于ReactiveCocoa的書(shū),所以我們可能要害死一些狗狗。。。

創(chuàng)建一個(gè)AppDelegate的屬性來(lái)hold住500px API客戶端

@property (nonatomic, readonly) PXAPIHelper * apiHelper;

application:didFinishLaunchingWithOptions:方法中實(shí)例化這個(gè)變量。

self.apiHelper = [[PXAPIHelper alloc]
                    initWithHost:nil
                    consumerKey:@"DC2To2BS0ic1ChKDK15d44M42YHf9gbUJgdFoF0m"
                    consumerSecret:@"i8WL4chWoZ4kw9fh3jzHK7XzTer1y5tUNvsTFNnB"];

我提供了一對(duì)一次性消費(fèi)的密鑰---請(qǐng)不要瘋到你也使用這對(duì)密鑰,你可以申請(qǐng)自己的。

好了,我們差不多也該建立數(shù)據(jù)的加載了。我們需要一個(gè)數(shù)據(jù)模型來(lái)hold住我們的信息。我創(chuàng)建了下面的FRPPhotoModel。

@interface FRPPhotoModel : NSObject
@property (nonatomic, strong) NSString *photoName;
@property (nonatomic, Strong) NSNumber *identifier;
@property (nonatomic, strong) NSString *photographerName;
@property (nonatomic, strong) NSNumber *rating;
@property (nonatomic, strong) NSString *thumbnailURL;
@property (nonatomic, strong) NSData *thumbnailData;
@property (nonatomic, strong) NSString *fullsizedURL;
@property (nonatomic, strong) NSData * fullsizedData;


@end

@implementation FRPPhotoModel

@end

非常好,到這里,我們將不直接在ViewController中加載內(nèi)容,相反,這部分邏輯將被抽象到另一個(gè)類中。創(chuàng)建一個(gè)名為FRPPhotoImporter的類。

到現(xiàn)在為止沒(méi)有一處代碼是關(guān)于函數(shù)式的。別擔(dān)心,我們就要這么做了!這個(gè)FRPPhotoImporter將不會(huì)真正返回一個(gè)FRPPhotoModel對(duì)象,相反他會(huì)返回一些隨身攜帶API最新的請(qǐng)求結(jié)果的信號(hào)。

@interface FRPPhotoImporter : NSObject
+ (RACSignal *)importPhotos;

@end

FRPPhotoImporterimportPhotos方法返回一個(gè)從API發(fā)送最新結(jié)果的RACSignal。這個(gè)RACSignal實(shí)際上是一個(gè)RACReplaySubject.但是由于ReactiveCocoa編程指南中不建議使用RACSubjects,我們申明的公共接口的返回類型為RACSignal而非RACSubject.現(xiàn)在讓我們繼續(xù)往下看:

+ (RACSignal *)importPhotos{
    RACReplaySubject * subject = [RACReplaySubject subject];
    NSURLRequest * request = [self popularURLRequest];
    [NSURLConnection sendAsynchronousRequest:request
                                    queue:[NSOperationQueue mainQueue]
                        completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError){
                            if (data) {
                                id results = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];

                                [subject sendNext:[[[results[@"photos"] rac_sequence] map:^id(NSDictionary *photoDictionary){
                                    FRPPhotoModel * model = [FRPPhotoModel new];

                                    [self configurePhotoModel:model withDictionary:photoDictionary];
                                    [self downloadThumbnailForPhotoModel:model];

                                    return model;
                                }] array]];

                                [subject sendCompleted];
                            }
                            else{
                                [subject sendError:connectionError];
                            }
    }];

    return subject;

}

這里面包含的內(nèi)容太多,我們慢慢來(lái)整理一下:

  • 首先我們創(chuàng)建了一個(gè)新的RACReplaySubject實(shí)例(這將是我們要返回的對(duì)象)。
  • 其次我們創(chuàng)建了一個(gè)NSURLRequest來(lái)獲取500px上熱門(mén)的FRPPhotoModel數(shù)據(jù)。
  • 隨后我們發(fā)送一個(gè)網(wǎng)絡(luò)的異步請(qǐng)求,并立即返回RACSubject對(duì)象。

這個(gè)直接返回的結(jié)果值得我們關(guān)注。

這個(gè)RACSubject對(duì)象被異步網(wǎng)絡(luò)請(qǐng)求的回調(diào)block捕獲,當(dāng)API接口返回?cái)?shù)據(jù)時(shí)回調(diào)block就會(huì)被調(diào)用,然后RACSubject對(duì)象會(huì)將結(jié)果傳送出來(lái),這些值將被我們的訂閱了RACSubject信號(hào)的接收者所接受。

這是你看到的異步操作中,一個(gè)非常普通的模式。

  1. 創(chuàng)建一個(gè)RACSubject.
  2. 從異步調(diào)用的完成block中向RACSubject傳送結(jié)果值。
  3. 立即返回這個(gè)RACSubject對(duì)象

重要的是,要注意一個(gè)普通的RASSubject及其子類RACReplaySubject之間的區(qū)別。RACReplaySubject可以確保他背后的Subject只會(huì)被訂閱一次,避免執(zhí)行重復(fù)的操作(就像上面這種網(wǎng)絡(luò)活動(dòng)的情況),RACReplaySubject將會(huì)緩存這個(gè)訂閱的值,并將其轉(zhuǎn)發(fā)給新的訂閱者們--- 對(duì)我們的需求來(lái)說(shuō)這非常完美。就像ReactiveCocoa的開(kāi)發(fā)者Justin Spahr-Summers所指出的,這也能夠避免可能的競(jìng)爭(zhēng)狀況。

我們發(fā)送了一個(gè)完整的數(shù)據(jù)集而不是單個(gè)隨時(shí)間變化的流。如果我們連環(huán)地發(fā)送一個(gè)個(gè)單獨(dú)的FRPPhotoModel流,這將'更加Reactive',也有助于實(shí)現(xiàn)分頁(yè)的需求,但是我們不打算采用這種方式,因?yàn)樗悬c(diǎn)點(diǎn)‘高級(jí)’了。你可以下載octokit:一個(gè)類似這種方式的例子。

URL請(qǐng)求的構(gòu)造方法看起來(lái)應(yīng)該是這樣的:

+ (NSURLRequest *)popularURLRequest {
    return [AppDelegate.apiHelper urlRequestForPhotoFeature:PXAPIHelperPhotoFeaturePopular
                resultsPerPage:100 page:0
                photoSize:PXPhotoModelSizeThumbnail
                sortOrder:PXAPIHelperSortOrderRating
                except:PXPhotoModelCategoryNude];
}

subject發(fā)送什么,完全看不到好嗎?呃。這取決于回調(diào)block.

if(data){
    id results = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
    [subject sendNext:[[[results[@"photos"] rac_sequence] map:^id (NSDictionary *photoDictionary){
        FRPPhotoModel *model = [FRPPhotoModel new];
        [self donwloadThumbnailForPhotoModel:model];

        return model;
    }] array]];

    [subject sendCompleted];
}
else{
    [subject sendError:connectionError];
}

測(cè)試是否有數(shù)據(jù)返回時(shí),可以說(shuō)這不是一個(gè)很好的錯(cuò)誤條件檢測(cè)的方法,但這是一個(gè)教學(xué)的例子。如果數(shù)據(jù)為nil,我們會(huì)發(fā)送一個(gè)errorValue,否則我們會(huì)反序列化JSON數(shù)據(jù)并處理它。這不太容易很快就看清楚是怎么做到的,讓我們來(lái)仔細(xì)看看。

[subject sendNext:[[[results[@"photos"] rac_sequence] map:^id (NSDictionary *photoDictionary){
    FRPPhotoModel * model = [FRPPhotoModel new];
    [self configurePhotoModel:model withDictionary:photoDictionary];
    [self downloadThumbnailForPhotoModel:model];
    return model;
}] array]];

[subject sendCompleted];

發(fā)送一個(gè)值,隨著subject擼過(guò)去,第一個(gè)表達(dá)式結(jié)構(gòu)相當(dāng)簡(jiǎn)潔(但是場(chǎng)景很典型)。這個(gè)值是photos的值,然后轉(zhuǎn)化為一個(gè)序列(sequence),然后做映射,最后轉(zhuǎn)化為一個(gè)數(shù)組。這是上一章介紹的非常簡(jiǎn)單的map技術(shù)。

這個(gè)map(映射)非常有意思。序列中的每一個(gè)元素,都會(huì)創(chuàng)建一個(gè)新的FRPPhotoModel對(duì)象、設(shè)置它然后返回它。為每一個(gè)results[ @"photos" ]的數(shù)組元素創(chuàng)建了一個(gè)FRPPhotoModel數(shù)組。這個(gè)數(shù)組就是隨著subject發(fā)送過(guò)來(lái)的值。最后我們發(fā)送一個(gè)完成值completedValue好讓訂閱者們知道任務(wù)完成了。

value_photoModel_map

注意在信號(hào)上手動(dòng)附送值的能力是非典型的,這是RACSubject實(shí)例的專屬能力。

configurePhotoModel:withDictionary:方法,看起來(lái)應(yīng)該像下面這樣:

+ (void)configurePhotoModel:(FRPPhotoModel *)photomodel withDictionary:(NSDictionary *)dictionary{
    //Basic details fetched with the first, basic request
    photomodel.photoname = dictionary[@"name"];
    photomodel.identifier = dictionary[@"id"];
    photomodel.photographerName = dictionary[@"user"][@"username"];
    photomodel.rating = dictionary[@"rating"];

    photomodel.thumbnailURL = [self urlForImageSize:3 inArray:dictionary[@"images"]];

    //Extended attributes fetched with subsequent request
    if (dictionary[@"comments_count"]){
        photomodel.fullsizedURL = [self urlForImageSize:4 inArray:dictionary[@"images"]];
    }
}

除了URL的屬性設(shè)置,都是最基本的東西。依靠其他的方法來(lái)從500px的API中返回的圖片列表中提取正確的url信息。500px API返回的數(shù)據(jù)結(jié)構(gòu)是下面這樣的格式:

(
    {
        size = size;
        url = ...;
    }
)

這是一個(gè)字典數(shù)組,每一個(gè)字典中包含一個(gè)size字段和一個(gè)url字段。我們讀取這樣字段的方法如下:

+ (NSString *)urlForImageSize:(NSInteger)size inDictionary:(NSArray *)array{
    return [[[[[array rac_sequence] filter:^ BOOL (NSDictionary * value){
        return [value[@"size"] integerValue] == size;
    }] map:^id (id value){
        return value[@"url"];
    }] array] firstObject];
}

這里有一些隱含的錯(cuò)誤處理,如果序列為空,NSArrayfirstObject方法默認(rèn)返回nil.

  • 第一步,我們過(guò)濾掉那些size字段不匹配要求的字典。
  • 然后,將這些符合要求的字典做一次映射來(lái)提取字典中url字段的內(nèi)容。
  • 最后,我們獲得一個(gè)NSString 對(duì)象的序列,把它轉(zhuǎn)化為數(shù)組,然后返回firstObject.

error_handling

在ReactiveCocoa中類似上面的鏈?zhǔn)秸{(diào)用非常常見(jiàn)。值從rac_sequence推送到filter:方法中,最后推送到map:方法里。最后調(diào)用序列rac_sequencearray方法,將序列的結(jié)果轉(zhuǎn)化為array.

最后,我們的downloadThumbnailForPhotoModel:方法,看起來(lái)應(yīng)該是下面這樣:

+ (void)downloadThumbnailForPhotoModel:(FRPPhotoModel *)photoModel{
    NSAssert(photoModel.thumbnailURL, @"Thumbnail URL must not be nil");

    NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:photoModel.ThumbnailURL]];
    [NSURLConnection sendAsynchronousRequest:request
        queue:[NSOperationQueue mainQueue]
        completionHandler:^(NSURLResponse *response, NSData *data, NSError * connectionError){
            photoModel.thumbnailData = data;
    }];
}

這個(gè)方法里面沒(méi)有任何的關(guān)于Reactive的部分---僅僅是下載thumbnail的url,然后在完成塊中適當(dāng)?shù)卦O(shè)置相關(guān)屬性。

我們幾乎做完了這個(gè)畫(huà)廊所需要的所有基礎(chǔ)的事情,接下來(lái),我們看看viewController.在實(shí)現(xiàn)文件里定義下面的的私有屬性。

@interface FRPGalleryViewController ()
@property (nonatomic , strong) NSArray *photoArray;

@end

來(lái)看下viewDidLoad中的實(shí)現(xiàn)。

static NSString * CellIdentifier = @"Cell";

- (void)viewDidLoad{
    [super ViewDidLoad];

    //Configure self
    self.title = @"Popular on 500px";

    //Configure View
    [self.collectionView registerClass:[FRPCell class] forCellWithReuseIdentifier:CellIdentifier];

    //Reactive Stuff
    @weakify(self);
    [RACObserver(self, photosArray) subscribeNext:^(id x){
        @strongify(self);
        [self.collectionView reloadData];
    }];

    //Load data
    [self loadPopularPhotos];
}

我們?yōu)関iewController設(shè)置了一個(gè)title并且為collectionView注冊(cè)了一個(gè)類,collectionView將會(huì)在他的cells中復(fù)用這個(gè)類的實(shí)例。這里我引用了一個(gè)不存在的UICollectionViewCell的子類,我們很快會(huì)創(chuàng)建她。

在'Reactive Stuff'注釋之下,你會(huì)發(fā)現(xiàn)一些奇怪的語(yǔ)法。

@weakify(self);
[RACObserver(self, photosArray) subscribeNext:^(id x){
    @strongify(self);
    [self.collectionView reloadData];
}];

RACObserver是一個(gè)C的宏定義,帶兩個(gè)參數(shù):對(duì)象及對(duì)象某個(gè)屬性的keyPath(關(guān)鍵路徑)。他會(huì)返回一個(gè)帶屬性值的信號(hào),無(wú)論這個(gè)屬性的值怎么變都會(huì)及時(shí)地通過(guò)該信號(hào)反饋出來(lái)。在這里當(dāng)self結(jié)束分配的時(shí)候會(huì)發(fā)送一個(gè)completion Value的值。訂閱這個(gè)信號(hào)的目的是無(wú)論我們的photosArray中的元素屬性怎么變,我們都能夠在collectionView重新加載的時(shí)候?qū)崟r(shí)獲取反饋。

在Objective-C的ARC條件下@weakify/@strongify這個(gè)雙人舞是非常常見(jiàn)的。@weakify創(chuàng)建一個(gè)新的self的弱引用weakself,@strongify創(chuàng)建這個(gè)weakself的強(qiáng)引用,并在@strongify的作用域中起作用。strongify的這種做法,一般稱為“影子變量”,那是因?yàn)檫@個(gè)新的強(qiáng)引用的變量就叫self,替代了原本強(qiáng)引用的self.

一般而言,subscribeNext:的block將捕獲其詞法范圍內(nèi)的self,造成self和block之間的循環(huán)引用。block被subscribeNext:的返回值,一個(gè)RACSubscriber實(shí)例,強(qiáng)引用,然后被RACObserver宏捕獲。解除分配時(shí),RACOberver會(huì)自動(dòng)解除第一個(gè)參數(shù)的分配,這樣的話self就應(yīng)該被解除分配,但self被block強(qiáng)引用,self要得以解除分配的唯一條件即引用計(jì)數(shù)為0,這樣的話就必須先解除block的分配,而前面的分析我們知道block被RACSubscriber實(shí)例引用,而該實(shí)例默認(rèn)被self強(qiáng)引用,因此,如果不調(diào)用weakify/strongify,self就永遠(yuǎn)也不可能解除分配。

最后,我們實(shí)際來(lái)調(diào)用loadPopularPhotos(他的實(shí)現(xiàn)如下)

- (void)loadPopularPhotos{
    [[FRPPhotoImporter importPhotos] subscribeNext:^(id x){
        self.photosArray = x;
    } error:^(NSError * error){
        NSLog(@"Couldn't fetch photofrom 500px: %@",error);
    }];
}

這個(gè)方法實(shí)際上負(fù)責(zé)調(diào)用FRPPhotoImporterimportPhotos方法(現(xiàn)在請(qǐng)加上他的頭文件),他訂閱了我們私有成員屬性的結(jié)果。由于UICollectionViewDataSource協(xié)議的架構(gòu),我們不得不把這些狀態(tài)引入進(jìn)來(lái)。

現(xiàn)在讓我們來(lái)看一下這些協(xié)議方法,有兩個(gè)是必須的,實(shí)現(xiàn)如下:

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
    return self.photosArray.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
    FRPCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
    [cell setPhotoModel:self.photosArray[indexPath.row]];

    return cell;
}

第一個(gè)方法簡(jiǎn)單地返回了collectionView中的cell的數(shù)量,在這里,準(zhǔn)確地講是photosArray屬性的cell數(shù)量。接下來(lái)的這個(gè)方法從collectionView列表中獲得了一個(gè)cell實(shí)例,并調(diào)用其上的setPhotoModel:方法(這個(gè)我們還沒(méi)有實(shí)現(xiàn),但別擔(dān)心)。這些代碼應(yīng)該看起來(lái)非常熟悉,如果你曾經(jīng)處理過(guò)UITableViewDataSource的方法的話。

這就是我們ViewController完整的實(shí)現(xiàn)。現(xiàn)在我們來(lái)創(chuàng)建UICollectionViewCell的子類,命名為FRPCell,像下面這樣來(lái)修改他的頭文件。

@class FRPPhotoModel;

@interface FRPCell : UICollectionViewCell
- (void)setPhotoModel:(FRPPhotoModel *)photoModel;
@end

在實(shí)現(xiàn)文件中添加下面的私有擴(kuò)展:

#import "FRPPhotoModel.h"
@interface FRPCell ()
@property (nonatomic , weak ) UIImageView * imageView;
@property (nonatomic , strong ) RACDisposeable *subscription;

@end

這里有兩個(gè)屬性:一個(gè)圖片視圖和一個(gè)訂閱者。圖片視圖是弱引用,因?yàn)樗鼘儆诟敢晥D(這是UICollectionViewCell的一個(gè)標(biāo)準(zhǔn)的用法),我們將實(shí)例化并賦值給imageView。接下來(lái)的屬性是一個(gè)訂閱,當(dāng)使用ReactiveCocoa來(lái)設(shè)置圖像視圖的圖像屬性時(shí),我們將接觸到它。注意它必須是強(qiáng)引用而非弱引用否則你會(huì)得到一個(gè)運(yùn)行時(shí)的異常。

- (id)initWithFrame:(CGRect)frame{
    self = [super initWithFrame:frame];
    if(!self) return nil;

    //Configure self
    self.backgroundColor = []UIColor darkGrayColor];

    //Configure subviews
    UIImageView * imageView = [[UIImageView alloc] initWithFrame:self.bounds];
    imageView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
    [self.contentView addsubView:imageView];
    self.imageView = imageView;

    return self;
}

標(biāo)準(zhǔn)的UICollectionView子類的模版會(huì)創(chuàng)建并分配imageView屬性。注意,我們必須有一個(gè)(被self)強(qiáng)引用的本地變量作為中介來(lái)存儲(chǔ)imageView,這樣就不會(huì)在賦值給self的imageView屬性的時(shí)候,imageView被立即解除分配。否則會(huì)有編譯錯(cuò)誤。

完成我們的500px畫(huà)廊,我們還需要實(shí)現(xiàn)兩個(gè)方法,第一個(gè)就是setPhotoModel:方法

- (void)setPhotoModel:(FRPPhotoModel *)photoModel{
    self.subscription = [[[RACObserver(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];
}

這種方法來(lái)給訂閱的屬性賦值,我們老早就知道了。它把setKeyPath:OnObject:的返回值賦給了self.subscription.實(shí)踐中這種方法根本不使用,我們使用RAC的C語(yǔ)法宏來(lái)代替,不久之后我們就會(huì)涉及這方面的知識(shí)。

兩個(gè)原因?qū)е掠嗛喪潜匾模?/p>

1. 當(dāng)它沒(méi)有接受一個(gè)新的值時(shí),我們想延遲處理。
2. 信號(hào)的訂閱通常是冷信號(hào),除非有人訂閱他(信號(hào)),否則信號(hào)不會(huì)起作用。

setKeyPath:onObject:RACSignal的一個(gè)方法:綁定最新的信號(hào)的值給對(duì)象的關(guān)鍵路徑。在這里我們?cè)谝粋€(gè)級(jí)聯(lián)的信號(hào)上調(diào)用了這個(gè)方法,讓我們來(lái)仔細(xì)看看:

[[RACObserver (photoModel, thumbnailData)
    filter:^BOOL (id value){
        return value != nil;
    }] map:^ id (id value){
        return [UIImage imageWithData:value];
    }];

chained_signal

信號(hào)由RACObserver這個(gè)C的宏生成,這個(gè)宏簡(jiǎn)單地返回一個(gè)監(jiān)控目標(biāo)對(duì)象關(guān)鍵路徑值變化的信號(hào)。在我們這個(gè)例子中,我們的目標(biāo)對(duì)象是photoModel,關(guān)鍵路徑為thumbnailData屬性。我們過(guò)濾掉所有的nil值,然后對(duì)過(guò)濾后的值做映射:把NSData實(shí)例轉(zhuǎn)為UIImage對(duì)象。

注意,把NSData實(shí)例轉(zhuǎn)化為UIImage的這個(gè)映射僅在小圖上可以很好地運(yùn)行,如果頻繁地做這個(gè)映射或者作用到大圖上會(huì)引起性能問(wèn)題。理想的情況下,我們會(huì)緩存這些已經(jīng)解壓的圖像以避免每一次都重復(fù)計(jì)算。這個(gè)技術(shù)不是本書(shū)所討論的范疇,但我們將使用另一個(gè)通過(guò)ReactiveCocoa來(lái)實(shí)現(xiàn)的方法。

thumbnailData屬性根本不需要在這里設(shè)置,他可以在稍后的某個(gè)時(shí)間在應(yīng)用的其他部分來(lái)完成設(shè)置,然后cell的圖像就會(huì)像魔術(shù)一般更新。

可以讓我們稍微突破一下Model-View-Controller模式好嗎?只是一點(diǎn)點(diǎn)的不守規(guī)矩。幸運(yùn)的是,下一章我們將看到無(wú)處不在的MVC模式的困境,所以我們不必?fù)?dān)心這一點(diǎn)點(diǎn)的突破,一點(diǎn)點(diǎn)的改進(jìn)。

上面提到的setKeyPath:onObject:方法中,一旦onObject:對(duì)象被釋放,他的訂閱也會(huì)被自動(dòng)取消。我們的cell實(shí)例是被collectionView所復(fù)用的,因此在復(fù)用的時(shí)候,我們需要取消cell上各組件的訂閱。我們可以通過(guò)重寫(xiě)UICollectionViewCell的下列方法達(dá)成:

- (void)perpareForReuse {
    [super prepareForReuse];

    [self.subscription dispose], self.subscription = nil;
}

這個(gè)方法在Cell被復(fù)用之前調(diào)用。如果現(xiàn)在運(yùn)行我的應(yīng)用,我們可以看到下面的結(jié)果:

disposing_subscription_works

太好了!我們可以通過(guò)滾動(dòng)視圖來(lái)證實(shí)我們手動(dòng)處理訂閱的有效性。

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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)