FunctionReactivePixels將會是一個簡單的觀看'500px'中最受歡迎的照片的應用。一旦我們完成這一節(jié),應用的主界面將會像下面這樣:
當然我們也可以像下圖一樣觀看全屏模式下的圖片。
這個App將使用Collection Views。如果你沒有太多這方面的經(jīng)驗,也不需要太過擔心---他們(CollectionView)就像TableView一樣,使用起來非常簡單。如果你對UICollectionView感興趣,可以閱讀我的另一本書.
我們將使用CocoaPods來管理我們的依賴,現(xiàn)在創(chuàng)建一個新的工程。我喜歡使用空模版以便我可以完全控制viewController層級。
首先、我們將創(chuàng)建一個UICollectionViewController的子類FRPGalleryViewController.同時我們創(chuàng)建一個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.這個flowlayout子類的實現(xiàn)非常簡單,只需要設置一些屬性就可以了。
@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)在屏幕上。為了實現(xiàn)這個,我們首先要在應用的application delegate的application: didFinishLaunchingWithOptions:
方法。我們想要將collectionview Controller置于一個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)在運行,我們將看到一個空視圖。
我們來填充一些內(nèi)容。創(chuàng)建一個Podfile文件,并填寫如下內(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
下一章,我們將添加一些測試?,F(xiàn)在運行pod install
,然后打開Xcode通用的workspace
文件。打開與編譯頭文件FRP-Prefix.pch
(Xcode6之后,新建工程默認不加載pch文件,需要自己添加,Apple的最佳實踐中已經(jīng)不推薦使用全局的預編譯pch文件),然后添加下面的內(nèi)容。這些語義會自動加載到項目的所有文件中。
//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])
對于這樣使用AppDelegate單例的用法,Saul Mora說:“每次看到你這么做,我家的狗都想死”。 但是這不是一本關于設計模式的書---這是一本關于ReactiveCocoa的書,所以我們可能要害死一些狗狗。。。
創(chuàng)建一個AppDelegate的屬性來hold住500px API客戶端
@property (nonatomic, readonly) PXAPIHelper * apiHelper;
在application:didFinishLaunchingWithOptions:
方法中實例化這個變量。
self.apiHelper = [[PXAPIHelper alloc]
initWithHost:nil
consumerKey:@"DC2To2BS0ic1ChKDK15d44M42YHf9gbUJgdFoF0m"
consumerSecret:@"i8WL4chWoZ4kw9fh3jzHK7XzTer1y5tUNvsTFNnB"];
我提供了一對一次性消費的密鑰---請不要瘋到你也使用這對密鑰,你可以申請自己的。
好了,我們差不多也該建立數(shù)據(jù)的加載了。我們需要一個數(shù)據(jù)模型來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)容,相反,這部分邏輯將被抽象到另一個類中。創(chuàng)建一個名為FRPPhotoImporter
的類。
到現(xiàn)在為止沒有一處代碼是關于函數(shù)式的。別擔心,我們就要這么做了!這個FRPPhotoImporter
將不會真正返回一個FRPPhotoModel
對象,相反他會返回一些隨身攜帶API最新的請求結果的信號。
@interface FRPPhotoImporter : NSObject
+ (RACSignal *)importPhotos;
@end
FRPPhotoImporter
的importPhotos
方法返回一個從API發(fā)送最新結果的RACSignal。這個RACSignal實際上是一個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)容太多,我們慢慢來整理一下:
RACReplaySubject
實例(這將是我們要返回的對象)。NSURLRequest
來獲取500px上熱門的FRPPhotoModel
數(shù)據(jù)。這個直接返回的結果值得我們關注。
這個RACSubject對象被異步網(wǎng)絡請求的回調(diào)block捕獲,當API接口返回數(shù)據(jù)時回調(diào)block就會被調(diào)用,然后RACSubject對象會將結果傳送出來,這些值將被我們的訂閱了RACSubject信號的接收者所接受。
這是你看到的異步操作中,一個非常普通的模式。
重要的是,要注意一個普通的RASSubject及其子類RACReplaySubject之間的區(qū)別。RACReplaySubject可以確保他背后的Subject只會被訂閱一次,避免執(zhí)行重復的操作(就像上面這種網(wǎng)絡活動的情況),RACReplaySubject將會緩存這個訂閱的值,并將其轉發(fā)給新的訂閱者們--- 對我們的需求來說這非常完美。就像ReactiveCocoa的開發(fā)者Justin Spahr-Summers所指出的,這也能夠避免可能的競爭狀況。
我們發(fā)送了一個完整的數(shù)據(jù)集而不是單個隨時間變化的流。如果我們連環(huán)地發(fā)送一個個單獨的FRPPhotoModel
流,這將'更加Reactive',也有助于實現(xiàn)分頁的需求,但是我們不打算采用這種方式,因為他有點點‘高級’了。你可以下載octokit:一個類似這種方式的例子。
URL請求的構造方法看起來應該是這樣的:
+ (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];
}
測試是否有數(shù)據(jù)返回時,可以說這不是一個很好的錯誤條件檢測的方法,但這是一個教學的例子。如果數(shù)據(jù)為nil
,我們會發(fā)送一個errorValue
,否則我們會反序列化JSON
數(shù)據(jù)并處理它。這不太容易很快就看清楚是怎么做到的,讓我們來仔細看看。
[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ā)送一個值,隨著subject擼過去,第一個表達式結構相當簡潔(但是場景很典型)。這個值是photos
的值,然后轉化為一個序列(sequence),然后做映射,最后轉化為一個數(shù)組。這是上一章介紹的非常簡單的map
技術。
這個map
(映射)非常有意思。序列中的每一個元素,都會創(chuàng)建一個新的FRPPhotoModel
對象、設置它然后返回它。為每一個results[ @"photos" ]
的數(shù)組元素創(chuàng)建了一個FRPPhotoModel
數(shù)組。這個數(shù)組就是隨著subject發(fā)送過來的值。最后我們發(fā)送一個完成值completedValue
好讓訂閱者們知道任務完成了。
注意在信號上手動附送值的能力是非典型的,這是RACSubject實例的專屬能力。
configurePhotoModel:withDictionary:
方法,看起來應該像下面這樣:
+ (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的屬性設置,都是最基本的東西。依靠其他的方法來從500px的API中返回的圖片列表中提取正確的url信息。500px API返回的數(shù)據(jù)結構是下面這樣的格式:
(
{
size = size;
url = ...;
}
)
這是一個字典數(shù)組,每一個字典中包含一個size
字段和一個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];
}
這里有一些隱含的錯誤處理,如果序列為空,NSArray
的firstObject
方法默認返回nil.
size
字段不匹配要求的字典。url
字段的內(nèi)容。firstObject
.在ReactiveCocoa中類似上面的鏈式調(diào)用非常常見。值從rac_sequence
推送到filter:
方法中,最后推送到map:
方法里。最后調(diào)用序列rac_sequence
的array
方法,將序列的結果轉化為array
.
最后,我們的downloadThumbnailForPhotoModel:
方法,看起來應該是下面這樣:
+ (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;
}];
}
這個方法里面沒有任何的關于Reactive
的部分---僅僅是下載thumbnail的url,然后在完成塊中適當?shù)卦O置相關屬性。
我們幾乎做完了這個畫廊所需要的所有基礎的事情,接下來,我們看看viewController
.在實現(xiàn)文件里定義下面的的私有屬性。
@interface FRPGalleryViewController ()
@property (nonatomic , strong) NSArray *photoArray;
@end
來看下viewDidLoad中的實現(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設置了一個title并且為collectionView注冊了一個類,collectionView將會在他的cells中復用這個類的實例。這里我引用了一個不存在的UICollectionViewCell的子類,我們很快會創(chuàng)建她。
在'Reactive Stuff'注釋之下,你會發(fā)現(xiàn)一些奇怪的語法。
@weakify(self);
[RACObserver(self, photosArray) subscribeNext:^(id x){
@strongify(self);
[self.collectionView reloadData];
}];
RACObserver
是一個C的宏定義,帶兩個參數(shù):對象及對象某個屬性的keyPath
(關鍵路徑)。他會返回一個帶屬性值的信號,無論這個屬性的值怎么變都會及時地通過該信號反饋出來。在這里當self結束分配的時候會發(fā)送一個completion Value
的值。訂閱這個信號的目的是無論我們的photosArray中的元素屬性怎么變,我們都能夠在collectionView重新加載的時候實時獲取反饋。
在Objective-C的ARC條件下@weakify/@strongify這個雙人舞是非常常見的。@weakify創(chuàng)建一個新的self的弱引用weakself,@strongify創(chuàng)建這個weakself的強引用,并在@strongify的作用域中起作用。strongify的這種做法,一般稱為“影子變量”,那是因為這個新的強引用的變量就叫self
,替代了原本強引用的self.
一般而言,subscribeNext:
的block將捕獲其詞法范圍內(nèi)的self,造成self和block之間的循環(huán)引用。block被subscribeNext:
的返回值,一個RACSubscriber實例,強引用,然后被RACObserver宏捕獲。解除分配時,RACOberver會自動解除第一個參數(shù)的分配,這樣的話self就應該被解除分配,但self被block強引用,self要得以解除分配的唯一條件即引用計數(shù)為0,這樣的話就必須先解除block的分配,而前面的分析我們知道block被RACSubscriber實例引用,而該實例默認被self強引用,因此,如果不調(diào)用weakify/strongify,self就永遠也不可能解除分配。
最后,我們實際來調(diào)用loadPopularPhotos
(他的實現(xiàn)如下)
- (void)loadPopularPhotos{
[[FRPPhotoImporter importPhotos] subscribeNext:^(id x){
self.photosArray = x;
} error:^(NSError * error){
NSLog(@"Couldn't fetch photofrom 500px: %@",error);
}];
}
這個方法實際上負責調(diào)用FRPPhotoImporter
的importPhotos
方法(現(xiàn)在請加上他的頭文件),他訂閱了我們私有成員屬性的結果。由于UICollectionViewDataSource協(xié)議的架構,我們不得不把這些狀態(tài)引入進來。
現(xiàn)在讓我們來看一下這些協(xié)議方法,有兩個是必須的,實現(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;
}
第一個方法簡單地返回了collectionView中的cell的數(shù)量,在這里,準確地講是photosArray屬性的cell數(shù)量。接下來的這個方法從collectionView列表中獲得了一個cell實例,并調(diào)用其上的setPhotoModel:
方法(這個我們還沒有實現(xiàn),但別擔心)。這些代碼應該看起來非常熟悉,如果你曾經(jīng)處理過UITableViewDataSource的方法的話。
這就是我們ViewController
完整的實現(xiàn)?,F(xiàn)在我們來創(chuàng)建UICollectionViewCell的子類,命名為FRPCell
,像下面這樣來修改他的頭文件。
@class FRPPhotoModel;
@interface FRPCell : UICollectionViewCell
- (void)setPhotoModel:(FRPPhotoModel *)photoModel;
@end
在實現(xiàn)文件中添加下面的私有擴展:
#import "FRPPhotoModel.h"
@interface FRPCell ()
@property (nonatomic , weak ) UIImageView * imageView;
@property (nonatomic , strong ) RACDisposeable *subscription;
@end
這里有兩個屬性:一個圖片視圖和一個訂閱者。圖片視圖是弱引用,因為它屬于父視圖(這是UICollectionViewCell的一個標準的用法),我們將實例化并賦值給imageView。接下來的屬性是一個訂閱,當使用ReactiveCocoa來設置圖像視圖的圖像屬性時,我們將接觸到它。注意它必須是強引用而非弱引用否則你會得到一個運行時的異常。
- (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;
}
標準的UICollectionView子類的模版會創(chuàng)建并分配imageView屬性。注意,我們必須有一個(被self)強引用的本地變量作為中介來存儲imageView,這樣就不會在賦值給self的imageView屬性的時候,imageView被立即解除分配。否則會有編譯錯誤。
完成我們的500px畫廊,我們還需要實現(xiàn)兩個方法,第一個就是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];
}
這種方法來給訂閱的屬性賦值,我們老早就知道了。它把setKeyPath:OnObject:
的返回值賦給了self.subscription
.實踐中這種方法根本不使用,我們使用RAC的C語法宏來代替,不久之后我們就會涉及這方面的知識。
兩個原因導致訂閱是必要的:
1. 當它沒有接受一個新的值時,我們想延遲處理。
2. 信號的訂閱通常是冷信號,除非有人訂閱他(信號),否則信號不會起作用。
setKeyPath:onObject:
是RACSignal
的一個方法:綁定最新的信號的值給對象的關鍵路徑。在這里我們在一個級聯(lián)的信號上調(diào)用了這個方法,讓我們來仔細看看:
[[RACObserver (photoModel, thumbnailData)
filter:^BOOL (id value){
return value != nil;
}] map:^ id (id value){
return [UIImage imageWithData:value];
}];
信號由RACObserver
這個C的宏生成,這個宏簡單地返回一個監(jiān)控目標對象關鍵路徑值變化的信號。在我們這個例子中,我們的目標對象是photoModel
,關鍵路徑為thumbnailData
屬性。我們過濾掉所有的nil值,然后對過濾后的值做映射:把NSData實例轉為UIImage對象。
注意,把NSData實例轉化為UIImage的這個映射僅在小圖上可以很好地運行,如果頻繁地做這個映射或者作用到大圖上會引起性能問題。理想的情況下,我們會緩存這些已經(jīng)解壓的圖像以避免每一次都重復計算。這個技術不是本書所討論的范疇,但我們將使用另一個通過ReactiveCocoa來實現(xiàn)的方法。
thumbnailData屬性根本不需要在這里設置,他可以在稍后的某個時間在應用的其他部分來完成設置,然后cell的圖像就會像魔術一般更新。
可以讓我們稍微突破一下Model-View-Controller模式好嗎?只是一點點的不守規(guī)矩。幸運的是,下一章我們將看到無處不在的MVC模式的困境,所以我們不必擔心這一點點的突破,一點點的改進。
上面提到的setKeyPath:onObject:
方法中,一旦onObject:
對象被釋放,他的訂閱也會被自動取消。我們的cell實例是被collectionView所復用的,因此在復用的時候,我們需要取消cell上各組件的訂閱。我們可以通過重寫UICollectionViewCell
的下列方法達成:
- (void)perpareForReuse {
[super prepareForReuse];
[self.subscription dispose], self.subscription = nil;
}
這個方法在Cell被復用之前調(diào)用。如果現(xiàn)在運行我的應用,我們可以看到下面的結果:
太好了!我們可以通過滾動視圖來證實我們手動處理訂閱的有效性。
更多建議: