本章的其他部分將把Functional Reactive Pixels Demo的其他代碼遷移到MVVM架構(gòu)中。我們將添加一個(gè)新的庫(kù)到Podfile文件里。Github上創(chuàng)作了ReactiveCocoa的黑客,也同時(shí)創(chuàng)建了一個(gè)ViewModel的基類:ReactiveViewModel.我們將要使用它的0.1.1版本。更新Podfile之后立即運(yùn)行pod install
以安裝該庫(kù)。
重構(gòu)的第一個(gè)類是高清圖片視圖控制器。從這兒開始是因?yàn)樗臉I(yè)務(wù)邏輯比較少,抽象成viewModel時(shí)相對(duì)簡(jiǎn)單。我們循序漸進(jìn),慢慢來。
目前,我們的FRPFullSizePhotoViewController
包含一個(gè)圖片數(shù)組和當(dāng)前圖片(在數(shù)組中)的下標(biāo)值。我們將把他們抽象到我們的視圖模型中來。
從頭文件中移除自定義初始化,追加FRPFullSizePhotoViewModel
的預(yù)申明。然后在這個(gè)新類中追加一個(gè)屬性。
@property (nonatomic ,strong ) FRPFullSizePhotoViewModel *viewModel;
在實(shí)現(xiàn)文件里,#import這個(gè)新的視圖模型(別擔(dān)心,我們很快就會(huì)創(chuàng)建它),
#import "FRPFullSizePhotoViewModel.h"
然后,移除photoModelArray
私有屬性的申明。重寫我們的初始化方法以移除對(duì)photoModelArray
實(shí)例的引用。代碼看起來應(yīng)該像下面這樣:
- (instancetype)init {
self = [super init];
if(!self) return nil;
//ViewControllers
self.pageViewController = [UIPageViewController alloc]
initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
options:@{ UIPageViewControllerOptionInterPageSpacingKey : @30 };
self.pageViewController.dataSource = self;
self.pageViewController.delegate = self;
[self addChildViewController:self.pageViewController];
return self;
}
在你的ViewDidLoad:
中添加如下代碼:
//Configure child view controllers
[self.pageViewController \
setViewControllers: @[ [self photoViewControllerForIndex:self.viewModel.initialPhotoIndex] ]
direction:UIPageViewControllerNavigationDirectionForward
animated:NO
completion:nil ];
//Configure self
self.title = [self.viewModel.initialPhotoModel photoName];
我們將要寫的這個(gè)我們提到的方法,對(duì)于veiwModel中發(fā)生的事情,給你一種XX感。最后,進(jìn)到photoViewControllerForIndex
方法中,它應(yīng)用了已經(jīng)解除分配的photoModelArray
,用下面的實(shí)現(xiàn)替代它。
- (FRPPhotoViewController *)photoViewControllerForIndex:(NSInteger)index {
if (index >= 0 && index < self.viewModel.photoArray.coung ) {
FRPPhotoModel *photoModel = self.viewModel.model[index];
FRPPhotoViewController *photoViewController = \
[[FRPPhotoViewController alloc] initWithPhotoModel:photoModel index:index];
return photoViewController;
}
// Index was out of bounds, return nil
return nil;
}
好了!現(xiàn)在輪到我們的視圖模型本身了。創(chuàng)建一個(gè)新的RVMViewModel
的子類,并將其命名為FRPFullSizedPhotoViewModel
.基于它將要封裝的信息,以及我們?cè)谝晥D控制器中的需求,我們知道,我們的頭文件看起來應(yīng)該是下面這樣:
@class FRPPhotoModel;
@interface FRPFullSizePhotoViewModel : RVMViewModel
- (instancetype)initWithPhotoArray:(NSArray *)photoArray initialPhotoIndex:(NSInteger)initialPhotoIndex;
- (FRPPhotoModel *)photoModelAtIndex:(NSInteger)index;
@property (nonatomic , readonly, strong) NSArray *model;
@property (nonatomic, readonly) NSInteger initialPhotoIndex;
@property (nonatomic, readonly) NSString *initialPhotoName;
@end
model
屬性在RVMViewModel
中被定義為id
類型,我們把它重定義為NSArray
. 我們也勾住了(即使用全局變量記錄)我們最初照片的索引(下標(biāo))并且給我們最初的照片名屬性定義了只讀屬性。這種微不足道的邏輯我們可以放到我們的視圖控制器中,但很快我們就會(huì)看到更為復(fù)雜的情況。
我們來完成實(shí)現(xiàn)文件里的東西。第一件事就是:我們需要#import FRPPhotoModel
類的頭文件。然后,我們將打開私有屬性的讀寫訪問權(quán)限。
//Model
#import "FRPPhotoModel.h"
@interface FRPFullSizePhotoViewModel ()
//private access
@property (nonatomic, assign) NSInteger initialPhotoIndex;
@end
好!下一步處理我們的初始化方法
- (instancetype)initWithPhotoArray:(NSArray *)photoArray initialPhotoIndex:(NSInteger)initialPhotoIndex {
self = [super initWithModel:photoArray];
if(!self) return nil;
self.initialPhotoIndex = initialPhotoIndex;
return self;
}
初始化方法中,先調(diào)用超類的initWithModel:
實(shí)現(xiàn),然后設(shè)置自己的initialPhotoIndex
屬性。剩下的兩個(gè)只讀屬性的獲取邏輯微不足道。
- (NSString *)initialPhotoName {
return [[self photoModelAtIndex:self.initialPhotoIndex] photoName];
}
- (FRPPhotoModel *)photoModelAtIndex:(NSInteger)index {
if(index < 0 || index > self.model.count - 1) {
//Index was out of bounds, return nil
return nil;
}
else {
return self.model[ index ];
}
}
這樣做的另一個(gè)優(yōu)點(diǎn)是:業(yè)務(wù)邏輯不需要重復(fù)書寫,而且也使得業(yè)務(wù)邏輯非常好進(jìn)行單元測(cè)試。
最后,我們需要在高清視圖控制器中設(shè)置該視圖模型,否則屏幕上將不會(huì)顯示任何東西。導(dǎo)航到我們的畫廊視圖控制器(那個(gè)我們實(shí)例化并推出高清視圖控制器的地方)。用下面的代碼來替換這個(gè)業(yè)務(wù)邏輯:
[[self rac_signalForSelector:@selector(collectionView:didSelectItemAtIndexPath:)
fromProtocol:@protocol(UIcollectionViewDelegate)] subscribeNext:^(RACTuple *arguments) {
@strongify(self);
NSIndexPath *indexPath = arguments.second;
FRPFullSizePhotoViewModel *viewModel = [[FRPPhotoViewModel alloc]
initWithPhotoArray:self.viewModel.model initialPhotoIndex:indexPath.item];
FRPFullSizePhotoViewController *viewController = [[FRPFullSizePhotoViewController alloc] init];
viewController.viewModel = viewModel;
viewController.delegate = (id)self;
[self.navigationController pushViewController:viewController animated:YES];
}];
在下一節(jié)開始之前,我們沒有計(jì)劃為視圖模型撰寫單元測(cè)試。下一節(jié)我們看到在視圖模型上如何運(yùn)行測(cè)試驅(qū)動(dòng)開發(fā)的概念?,F(xiàn)在我們來完成FRPGalleryViewModel
吧,很基礎(chǔ)。我們想要從視圖控制器中抽象出來的邏輯是通過API加載model
的數(shù)據(jù)內(nèi)容。我們來看一下應(yīng)該怎么做:
@interface FRPGalleryViewModel : RVMViewModel
@property (nonatomic, readonly, strong) NSArray *model;
@end
基本的接口:將model
申明為數(shù)組NSArray
.接下來,我們簡(jiǎn)單實(shí)現(xiàn)它:
//Utilities
#import "FRPPhotoImporter.h"
@interface FRPGalleryViewModel ()
@end
@implementation FRPGalleryViewModel
- (instancetype)init {
self = [super init];
if(!self) return nil;
RAC(self, model) = [[[FRPPhotoImporter importPhotos] logError] catchTo:[RACSignal empty]];
return self;
}
@end
有爭(zhēng)議的是,我們應(yīng)該把從API加載數(shù)據(jù)的(RAC綁定的)邏輯放在初始化方法中,還是放在視圖模型被激活的地方。接下來我們會(huì)討論更多的關(guān)于激活的內(nèi)容,但我想要展示給你們看這個(gè)視圖模型到底能做到多簡(jiǎn)單。將直接在畫廊視圖控制器中加載數(shù)據(jù)內(nèi)容的邏輯遷移到畫廊的視圖模型中是非常簡(jiǎn)單的:在視圖控制器的初始化中初始化視圖模型===》任何引用試圖控制self.model
屬性的地方使用self.viewModel.model
來代替即可。
我們可以進(jìn)一步深挖視圖模型的構(gòu)造,甚至可以通過一系列的訪問器把model
的訪問邏輯抽象出來,但在這個(gè)例子里就有點(diǎn)過多‘抽象’了。更重要的是你可以根據(jù)你的喜好將更多的或者更少的業(yè)務(wù)邏輯抽象到視圖模型中。我發(fā)現(xiàn),就我個(gè)人而言,這個(gè)架構(gòu)使用的越多,業(yè)務(wù)邏輯抽象出來的越多,就意味著更輕量級(jí)的視圖控制器以及高內(nèi)聚和可測(cè)試的代碼。
把注意力移到單元測(cè)試之前,我們來做多一次用視圖模型來抽象業(yè)務(wù)邏輯的實(shí)踐。
我們的最后一個(gè)例子是FRPPhotoViewController
上的FRPPhotoViewModel
:創(chuàng)建一個(gè)RVMViewModel
的視圖模型子類并放置在視圖控制器中(很快我們會(huì)回到視圖模型中)。
視圖控制器的新的初始化方法如下:
- (instancetype)initWithViewModel:(FRPPhotoViewModel *)viewModel index:(NSInteger)photoIndex {
self = [self init];//NS_DESIGNATED_INITIALIZER
if(!self) return nil;
self.viewModel = viewModel;
self.photoIndex = photoIndex;
return self;
}
確定導(dǎo)入必要的頭文件并為視圖模型申明私有屬性?,F(xiàn)在我們需要使用新的初始化方法初始化視圖控制器??匆豢匆晥D控制器到頁面視圖控制器的方法photoViewControllerForIndex:
.
- (FRPPhotoViewController *)photoViewControllerForIndex:(NSInteger)index {
FRPPhotoModel *photoModel = [self.viewModel photoModelAtIndex:index];
if(photoModel) {
FRPPhotoViewModel *photoViewModel = [[FRPPhotoViewModel alloc] initWithModel:photoModel];
FRPPhotoViewController *photoViewController = [[FRPPhotoViewController alloc] \
initWithViewModel:photoViewModel
index:index];
return photoViewController;
}
return nil;
}
新的初始化過程中我們創(chuàng)建了一個(gè)視圖模型。
在我們的viewDidLoad:
方法里,我們將使用這個(gè)新的視圖模型為我們的圖片視圖提供數(shù)據(jù),并且為用戶顯示圖片的下載進(jìn)度。這里有個(gè)貌似沖突的地方:圖片的下載是視圖的模型的業(yè)務(wù)邏輯之一,但視圖什么時(shí)候顯示開始加載數(shù)據(jù)(這個(gè)業(yè)務(wù)邏輯)視圖模型中沒有體現(xiàn)---記住一個(gè)好的視圖模型不應(yīng)該引用視圖本身。那么我們?nèi)绾蝸砘旌系厥褂眠@兩個(gè)業(yè)務(wù)邏輯?
答案是我們借助視圖模型的active
狀態(tài)來對(duì)付(上面的情況)。RVMViewModel
提供了一個(gè)布爾屬性active
,當(dāng)試圖控制器變得"活躍"時(shí)(不管在語義的上下文里這是啥意思),在這里,我們可以在viewWillAppear:
和viewDidDisappear:
這些方法來設(shè)置這個(gè)屬性。
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.viewModel.active = YES;
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
self.viewModel.active = NO;
}
相當(dāng)簡(jiǎn)單吧,我們來看一下我們新的viewDidLoad
方法:
- (void)viewDidLoad {
[super viewDidLoad];
//Configure self's view
self.view.backgroundColor = [UIColor blackColor];
//Configure subViews
UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
RAC(imageView, image) = RACObserve(self.viewModel,photoImage);
imageView.contentModel = UIViewContentModelScaleAspectFit;
[self.view addSubView:imageView];
self.imageView = imageView;
[RACObserve(self.viewModel, loading) subscribeNext:^(NSNumber *loading) {
if(loading.boolValue) {
[SVProgressHUD show];
}
else {
[SVProgressHUD dismiss];
}
}];
}
該圖片視圖的圖片屬性的綁定是標(biāo)準(zhǔn)的ReactiveCocoa方式,有趣的是下面(我們要提到的)我們使用loading
的時(shí)刻。當(dāng)加載信號(hào)發(fā)送YES
的時(shí)候我們展示進(jìn)度HUD,發(fā)送NO
的時(shí)候,讓進(jìn)度HUD消失。我們將看到該loading
信號(hào)本身如何依賴于didBecomeActiveSignal
?,F(xiàn)在只是視圖模型通過網(wǎng)絡(luò)請(qǐng)求獲取圖像數(shù)據(jù)的序幕。
接口的申明如下:
@class FRPPhotoModel;
@interface FRPPhotoViewModel : RVMViewModel
@property (nonatomic, readonly) FRPPhotoModel *model;
@property (nonatomic, readonly) UIImage *photoImage;
@property (nonatomic, readonly, getter = isLoading) BOOL loading;
- (NSString *)photoName;
@end
該model
和photoImage
屬性的用法已經(jīng)解釋過了。photoName
事實(shí)上作為屬性在代碼庫(kù)的其他地方被用來設(shè)置一些東西,類似于分頁視圖控制器的標(biāo)題這樣。你可以下載Github的代碼庫(kù)了解詳情。我們來看一下實(shí)現(xiàn):
#import "FRPPhotoViewModel.h"
//Utilities
#import "FRPPhotoImporter.h"
#import "FRPPhotoModel.h"
@interface FRPPhotoViewModel ()
@property (nonatomic, strong) UIImage *photoImage;
@property (nonatomic, assign, getter = isLoading) BOOL loading;
@end
@implementation FRPPhotoViewModel
- (instancetype)initWithModel:(FRPPhotoModel *)photoModel {
self = [super initWithModel:photoModel];
if(!self) return nil;
@weakify(self);
[self.didBeComeActiveSignal subscribeNext:^(id x) {
@strongify(self);
self.loading = YES;
[[FRPPhotoImporter fetchPhotoDetails:self.model] subscribeError:^(NSError *error) {
NSLog(@"Could not fetch photo details: %@",error);
} completed:^{
self.loading = NO;
NSLog(@"Fetched photoDetails.");
}];
}];
RAC(self, photoImage) = [RACObserve(self.model, fullsizedData) map:^id (id value) {
return [UIImage imageWithData:value];
}];
return self;
}
- (NSString *)photoName {
return self.model.photoName;
}
@end
該didBecomeActive
信號(hào)訂閱帶有"函數(shù)副作用"的加載照片詳情包括它的高清圖片的數(shù)據(jù)。然后photoImage
屬性與模型的映射結(jié)果綁定。
使用didBecomeActiveSignal
這種方法來啟動(dòng)一些像網(wǎng)絡(luò)操作這樣昂貴的任務(wù),遠(yuǎn)遠(yuǎn)優(yōu)于我們?cè)缜霸诔跏蓟椒ㄖ袉?dòng)他們的方法。
本書要介紹的全部?jī)?nèi)容已經(jīng)講完了,更多的內(nèi)容請(qǐng)參考functional reactive pixels,這個(gè)代碼庫(kù)包含了更多的在圖片詳情視圖控制器和登陸視圖控制器中使用視圖模型的例子。這些Demo將向你展示如何有效地使用ReactiveCocoa
執(zhí)行網(wǎng)絡(luò)操作和使用RACCommands
響應(yīng)用戶界面交互。
更多建議: