原文:MVVM Tutorial with ReactiveCocoa: Part 1/2
你可能之前在推特看過這么一個(gè)笑話:
*“iOS Architecture, where MVC stands for Massive View Controller” *
這是對(duì)iOS開發(fā)者的一個(gè)小小調(diào)侃棵譬,但我敢說(shuō)你們肯定都在實(shí)踐中遇到過這樣的問題:臃腫而又難于管理的視圖控制器呼股。
這篇教程將介紹一個(gè)構(gòu)造應(yīng)用的嶄新模式:Model-View-ViewModel(MVVM)廓推。這種得益于ReactiveCocoa的模式為我們提供了一個(gè)優(yōu)異的MVC替代方案惧蛹,讓視圖控制器更加有序和輕量囤萤。
在這篇MVVM的教程中,你將要構(gòu)建一個(gè)簡(jiǎn)單的Flickr搜索應(yīng)用,效果如下:
注意:這篇教程基于Objective-C,并非Swift封锉。同時(shí)開發(fā)環(huán)境使用的是Xcode 5,而非Xcode 6,所以所有的截圖都從Xcode 5中截取成福。為了得到最好的效果碾局,請(qǐng)使用Xcode 5來(lái)完成本教程。(譯注:因?yàn)槭?4年的文章奴艾,所以Xcode版本比較老舊净当,但具體界面操作還是大同小異)
但無(wú)路如何,如果你實(shí)在對(duì)這個(gè)示例項(xiàng)目的Swift版本感興趣的話蕴潦,你可以關(guān)注我最近的博客中的實(shí)現(xiàn)O裉洹(譯注:因?yàn)镾wift幾個(gè)版本以來(lái)API和語(yǔ)法更新較多,ReactiveCocoa對(duì)應(yīng)也有較大的更新潭苞,當(dāng)時(shí)的代碼估計(jì)已經(jīng)不再適用)
在編寫代碼之前忽冻,我們先來(lái)學(xué)習(xí)一點(diǎn)理論知識(shí)。
ReactiveCocoa的簡(jiǎn)短回顧
這篇教程是主要介紹MVVM此疹,并默認(rèn)你已經(jīng)掌握了ReactiveCocoa的相關(guān)知識(shí)點(diǎn)僧诚。如果你之前從來(lái)沒有使用過ReactiveCocoa,我強(qiáng)烈建議你閱讀一下早前相關(guān)的教程秀菱。
如果你需要對(duì)ReactiveCocoa進(jìn)行快速?gòu)?fù)習(xí)振诬,下面我會(huì)簡(jiǎn)短地回顧一些關(guān)鍵點(diǎn)。
ReactiveCocoa最核心的就是信號(hào)衍菱,對(duì)應(yīng)表現(xiàn)為RACSignal
類赶么。信號(hào)發(fā)送事件流,而事件流又分三種類型:next
脊串、completed
和error
辫呻。
使用這種簡(jiǎn)單的模型嘉汰,ReactiveCocoa可以對(duì)代理模式, target-action模式谱净,鍵值對(duì)觀察(KVO)等進(jìn)行替代。
由于信號(hào)創(chuàng)建的接口更加同質(zhì)(homogenous)颅拦,所以代碼更加易讀缕坎。但ReactiveCocoa更強(qiáng)大的地方體現(xiàn)在對(duì)這些信號(hào)的高級(jí)操作怖侦。那讓你可以用相當(dāng)簡(jiǎn)潔的方式實(shí)現(xiàn)復(fù)雜的過濾,轉(zhuǎn)換和信號(hào)協(xié)調(diào)谜叹。
ReactiveCocoa在MVVM的實(shí)現(xiàn)中是一個(gè)特殊的存在匾寝。它是ViewModel和View的“粘合劑”。但這概念對(duì)現(xiàn)在的你來(lái)說(shuō)可能有點(diǎn)超前了……
MVVM的介紹
眾所周知荷腊,Model-View-ViewModel(MVVM)是一中UI設(shè)計(jì)模式艳悔。它是MV*模式眾多成員中的一份子,其他還包括Model View Controller (MVC)女仰,Model View Presenter (MVP)等猜年。
這些模式都關(guān)注于將UI邏輯從業(yè)務(wù)邏輯中分離出來(lái)抡锈,讓應(yīng)用更易于開發(fā)與測(cè)試。
注意:想了解更多常見的設(shè)計(jì)模式乔外,我推薦Eli或者Ash Furrow的文章床三。
為了更好理解MVVM模式,我們可以回顧一下它的起源袁稽。
MVC是第一個(gè)UI設(shè)計(jì)模式勿璃,它的起源可以追溯到二十世紀(jì)七十年代的Smalltalk
語(yǔ)言。下圖展示了MVC模式的主要組成:
這個(gè)模式將UI視圖分解為三個(gè)部分:表示應(yīng)用狀態(tài)的Model推汽,UI控件組成的View补疑,和根據(jù)情況處理交互和更新模型的Controller。
而MVC模式最大的問題就是它迷惑性歹撒。這概念雖然看起來(lái)很美莲组,但當(dāng)人們開始實(shí)現(xiàn)MVC時(shí),Model暖夭,View和 Controller之間就會(huì)形成上圖中相似的循環(huán)關(guān)系锹杈。換言之,這導(dǎo)致了一種可怕的混亂迈着。
而最近Martin Fowler提出了一種稱為Presentation Model的MVC變體竭望,這種變體被微軟以MVVM為名,采納并普及開來(lái)裕菠。
這種模式的核心是ViewModel咬清,一種用以代表應(yīng)用UI界面的狀態(tài)的特殊模型。
其包含了每個(gè)UI控件的狀態(tài)屬性奴潘。例如旧烧,文本輸入框的實(shí)時(shí)內(nèi)容,或者指定按鈕的可用與否画髓。它也暴露了視圖的具體的可用操作掘剪,比如按鈕的點(diǎn)擊或手勢(shì)。
可以將ViewModel理解為視圖的模型(model-of-the-view)奈虾。
MVVM模式中三個(gè)組件間的關(guān)系比MVC中的更加簡(jiǎn)單夺谁,具體遵循這些嚴(yán)格的規(guī)定:
- View能引用ViewModel,但ViewModel不能引用View肉微。
- ViewModel能引用Model匾鸥,但Model不能引用ViewModel。
只要你違反了其中一條規(guī)定浪册,那么你就沒有正確的使用MVVM扫腺!
這種模式有兩個(gè)立竿見影的好處:
- 輕量級(jí)的視圖:所有的UI邏輯都遷移到ViewModel中岗照,剩下的視圖非常輕量村象。
- 方便測(cè)試:你可以脫離視圖運(yùn)行應(yīng)用邏輯笆环,顯著地提升了可測(cè)試性。
注意:眾所周知厚者,視圖測(cè)試相當(dāng)困難躁劣,因?yàn)槟呐率窃傩〉囊粋€(gè)測(cè)試都會(huì)涉及大量不相關(guān)的代碼。通常來(lái)說(shuō)库菲,控制器會(huì)對(duì)依賴于其他的應(yīng)用狀態(tài)的場(chǎng)景添加和設(shè)置視圖账忘。這意味著有效的小型測(cè)試將是一個(gè)脆弱而繁瑣的命題。
此時(shí)你可能會(huì)提出一個(gè)問題熙宇。如果View只能單向引用ViewModel鳖擒,ViewModel又如何更新View呢?
這正是MVVM模式的秘訣烫止。
MVVM和數(shù)據(jù)綁定
MVVM模式依賴于數(shù)據(jù)綁定這樣一個(gè)框架級(jí)的功能蒋荚,用以自動(dòng)連接對(duì)象屬性和UI控件。
舉個(gè)例子馆蠕,在微軟的WPF框架中期升,下面的標(biāo)簽綁定了文本框的Text屬性和ViewModel的Username屬性:
<TextField Text=”{DataBinding Path=Username, Mode=TwoWay}”/>
WPF框架讓這兩個(gè)屬性關(guān)聯(lián)在一起。
這種雙向綁定一方面讓Username的屬性改動(dòng)得以傳遞到文本框的Text
屬性中互躬,而另一方面播赁,又能把用戶的輸入反映給ViewModel。
在另一個(gè)流行的網(wǎng)頁(yè)端MVVM框架Knockout中吼渡,你也可以看到相似的綁定操作:
<input data-bind=”value: username”/>
上面的代碼就把一個(gè)HTML元素的屬性與一個(gè)JavaScript對(duì)象綁定起來(lái)容为。
很不幸,iOS缺乏這樣一個(gè)數(shù)據(jù)綁定的框架诞吱,但這正是ReactiveCocoa發(fā)揮“粘合劑”作用的地方舟奠,將ViewModel進(jìn)行關(guān)聯(lián)。
現(xiàn)在特別從iOS開發(fā)的角度研究一下MVVM模式房维。我們能發(fā)現(xiàn)視圖控制器和它關(guān)聯(lián)的UI(無(wú)論是nib沼瘫,stroyboard還是通過代碼創(chuàng)建的)共同組成了View部分,而ReactiveCocoa則將View與ViewModel綁定起來(lái):
注意:想了解更多關(guān)于UI模式的歷史分析咙俩,我強(qiáng)烈推薦Martin Fowler的Martin Fowler’s GUI Architectures耿戚。
你是不是已經(jīng)受夠這些理論了?好吧阿趁,如果不是你可以往上再重讀一遍膜蛔。哦?你都已經(jīng)掌握了?好吧脖阵,那接下來(lái)是時(shí)候創(chuàng)建你自己的ViewModel了皂股。
初始的項(xiàng)目結(jié)構(gòu)
首先下載下面的初始項(xiàng)目:
項(xiàng)目使用CocoaPods去管理依賴包(如果你是第一次接觸CocoaPods,我們?yōu)槟銣?zhǔn)備了一個(gè)新手教程C)呜呐。執(zhí)行pod install
命令去獲取依賴就斤,確認(rèn)你能看到下面的輸出:
譯注:由于當(dāng)時(shí)CocoaPods的版本較低,現(xiàn)今Podfile文件的格式有所改變蘑辑,更新如下:
platform :ios, '7.0'
target 'RWTFlickrSearch' do
pod 'ReactiveCocoa', '2.1.8'
pod 'objectiveflickr', '2.0.4'
pod 'LinqToObjectiveC', '2.0.0'
pod 'SDWebImage', '3.6'
end
$ pod install
Analyzing dependencies
Downloading dependencies
Installing LinqToObjectiveC (2.0.0)
Installing ReactiveCocoa (2.1.8)
Installing SDWebImage (3.6)
Installing objectiveflickr (2.0.4)
Generating Pods project
Integrating client project
你會(huì)在接下來(lái)的使用中學(xué)到更多關(guān)于這些依賴包的的內(nèi)容洋机。
譯注:這個(gè)版本(3.6)的SDWebImage在Xcode6及后續(xù)版本有報(bào)錯(cuò),請(qǐng)?jiān)?code>@implementation SDWebImageDownloaderOperation {...}后添加:
@synthesize executing = _executing ;
@synthesize finished = _finished;
這個(gè)教程的初始工程已經(jīng)包含了用視圖控制器和nib文件實(shí)現(xiàn)的應(yīng)用視圖洋魂。打開由CocoaPods 生成的RWTFlickrSearch.xcworkspace
文件绷旗,編譯運(yùn)行初始項(xiàng)目后你就能看到這些視圖中的一個(gè):
花點(diǎn)時(shí)間熟悉一下項(xiàng)目的架構(gòu):
Model和ViewModel分組中現(xiàn)在為空;你很快將往里面添加文件副砍。而View分組包含內(nèi)容如下:
-
RWTFlickSearchViewController
:應(yīng)用的主界面衔肢,包含一個(gè)搜索文本框和“Go”按鈕。 -
RWTRecentSearchItemTableViewCell
:在主屏幕中展示最新的搜索結(jié)果的列表單元格豁翎。 -
RWTSearchResultsViewController
:搜索結(jié)果界面膀懈,展示來(lái)自Flickr的圖片列表。 -
RWTSearchResultsTableViewCell
:展示單幅Flickr圖片的列表單元格
現(xiàn)在可以添加你的第一個(gè)ViewModel了谨垃!
你的第一個(gè)ViewModel
在ViewModel分組下添加一個(gè)新類启搂,命名為RWTFlickrSearchViewModel
并繼承自NSObject
。
打開新添加的頭文件刘陶,并往里面添加如下兩個(gè)屬性:
@interface RWTFlickrSearchViewModel : NSObject
@property (strong, nonatomic) NSString *searchText;
@property (strong, nonatomic) NSString *title;
@end
searchText
屬性代表著文本框顯示的文本胳赌,而title
屬性則表示導(dǎo)航條中的顯示標(biāo)題。
注意:為了更容易理解應(yīng)用的架構(gòu)匙隔,View和ViewModel都共用一個(gè)只有后綴不同的名字疑苫。比如
RWTFlickrSearch-ViewModel
和RWTFlickrSearch-ViewController
。
打開RWTFlickrSearchViewModel.m
并添加以下代碼:
@implementation RWTFlickrSearchViewModel
- (instancetype)init {
self = [super init];
if (self) {
[self initialize];
}
return self;
}
- (void)initialize {
self.searchText = @"search text";
self.title = @"Flickr Search";
}
@end
這簡(jiǎn)單的設(shè)置了ViewModel的初始狀態(tài)纷责。
下一步是將ViewModel和View關(guān)聯(lián)起來(lái)捍掺。記得View持有ViewModel的引用。在這種情況下再膳,最好為View添加初始化方法挺勿,賦予對(duì)應(yīng)的ViewModel。
注意:在本教程中我們把控制器也視為“View”的一部分喂柒,以更符合MVVM中“View”的語(yǔ)義不瓶。這有別于UIKit中一貫的用法。
打開RWTFlickrSearchViewController.h
并導(dǎo)入ViewModel的頭文件:
#import "RWTFlickrSearchViewModel.h"
然后添加初始方法如下:
@interface RWTFlickrSearchViewController : UIViewController
- (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel;
@end
在RWTFlickrSearchViewController.m
類擴(kuò)展的UI引用后添加私有屬性如下:
@property (weak, nonatomic) RWTFlickrSearchViewModel *viewModel;
然后添加初始方法如下:
- (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel {
self = [super init];
if (self ) {
_viewModel = viewModel;
}
return self;
}
這存儲(chǔ)了這個(gè)View對(duì)應(yīng)的ViewModel引用灾杰。
注意:這是一個(gè)弱引用蚊丐;View引用ViewModel,但并不持有它艳吠。
在viewDidLoad
方法的末端添加代碼如下:
[self bindViewModel];
然后實(shí)現(xiàn)這個(gè)方法如下:
- (void)bindViewModel {
self.title = self.viewModel.title;
self.searchTextField.text = self.viewModel.searchText;
}
上面的代碼會(huì)在UI初始化時(shí)執(zhí)行麦备,并把ViewModel的狀態(tài)賦予給View。
最后一步就是創(chuàng)建ViewModel的實(shí)例,并提供給View凛篙。
在RWTAppDelegate.m
添加引用如下:
#import "RWTFlickrSearchViewModel.h"
并添加私有屬性(在文件上方的類擴(kuò)展中):
@property (strong, nonatomic) RWTFlickrSearchViewModel *viewModel;
你會(huì)發(fā)現(xiàn)這個(gè)類中已經(jīng)有一個(gè)createInitialViewController
方法弄屡,更新它的實(shí)現(xiàn)如下:
- (UIViewController *)createInitialViewController {
self.viewModel = [RWTFlickrSearchViewModel new];
return [[RWTFlickrSearchViewController alloc] initWithViewModel:self.viewModel];
}
這創(chuàng)建了一個(gè)ViewModel的實(shí)例,然后構(gòu)造返回了View鞋诗。這就是應(yīng)用導(dǎo)航控制器的初始視圖。
編譯運(yùn)行迈嘹,現(xiàn)在視圖已經(jīng)具有了一些新的狀態(tài)削彬。
恭喜,這就是你的第一個(gè)ViewModel秀仲。但請(qǐng)先收斂一下你的興奮之情融痛,接下來(lái)還有很多東西要學(xué)呢 ; ]
你可能已經(jīng)發(fā)現(xiàn),你還沒有使用任何的ReactiveCocoa神僵。在現(xiàn)在的情況下雁刷,用戶在搜索文本框輸入的任何內(nèi)容都不會(huì)反映到ViewModel中。
檢測(cè)搜索有效性
在這個(gè)部分保礼,你將會(huì)使用ReactiveCocoa綁定ViewModel和View沛励,以便把搜索文本框和按鈕都與ViewModel關(guān)聯(lián)起來(lái)。
在RWTFlickrSearchViewController.m
更新bindViewModel
方法如下:
- (void)bindViewModel {
self.title = self.viewModel.title;
RAC(self.viewModel, searchText) = self.searchTextField.rac_textSignal;
}
ReactiveCocoa使用category為UITextField
添加了rac_textSignal
屬性炮障。這個(gè)信號(hào)會(huì)在文本框更新時(shí)發(fā)送一個(gè)包含現(xiàn)有文本的next事件目派。
RAC
宏是一個(gè)綁定操作。上面的代碼會(huì)在rac_textSignal
發(fā)送的next事件時(shí)用里面的內(nèi)容更新viewModel
的searchText
屬性胁赢。
簡(jiǎn)單來(lái)說(shuō)企蹭,這保證了searchText
總是反映著當(dāng)前的UI狀態(tài)。如果上面的內(nèi)容讓你覺得云里霧里智末,那么你真的需要復(fù)習(xí)一下之前的兩篇ReactiveCocoa教程了谅摄!
搜索按鈕應(yīng)該只有在用戶輸入的文本有效時(shí)才可用。為了簡(jiǎn)化流程系馆,我們暫時(shí)規(guī)定只有當(dāng)輸入的文本超過三個(gè)字節(jié)時(shí)才能執(zhí)行搜索送漠。
在RWTFlickrSearchViewModel.m
添加導(dǎo)入如下:
#import <ReactiveCocoa/ReactiveCocoa.h>
之后更新初始化方法如下:
- (void)initialize {
self.title = @"Flickr Search";
RACSignal *validSearchSignal =
[[RACObserve(self, searchText)
map:^id(NSString *text) {
return @(text.length > 3);
}]
distinctUntilChanged];
[validSearchSignal subscribeNext:^(id x) {
NSLog(@"search text is valid %@", x);
}];
}
編譯運(yùn)行并在文本框中任意輸入一些內(nèi)容。現(xiàn)在當(dāng)文本內(nèi)容在有效和無(wú)效間轉(zhuǎn)換時(shí)你就能在控制臺(tái)看到打印信息:
2014-05-27 18:03:26.299 RWTFlickrSearch[13392:70b] search text is valid 0
2014-05-27 18:03:28.379 RWTFlickrSearch[13392:70b] search text is valid 1
2014-05-27 18:03:29.811 RWTFlickrSearch[13392:70b] search text is valid 0
上面的代碼使用RACObserve
宏為ViewModel的searchText
屬性創(chuàng)建了一個(gè)信號(hào)(這是ReactiveCocoa對(duì)KVO的封裝)由蘑。隨后map操作將文本轉(zhuǎn)化為值為布爾值的流螺男。
最后,distinctUntilChanges
方法用于保證該信號(hào)只有在狀態(tài)變更時(shí)才會(huì)發(fā)送新值纵穿。
注意:如果你在過程中感到困難下隧,嘗試將這個(gè)過程分解。先添加
RACObserve
宏谓媒,打印輸出淆院,然后再添加map操作和distinctUntilChanged
方法。
至今ReactiveCocoa主要用于綁定View和ViewModel,以確保兩者保持同步土辩。而除此之外支救,ReactiveCocoa也在ViewModel的內(nèi)部用于觀察它自身的state和執(zhí)行其他操作。
這種模式在本教程隨處可見拷淘。ReactiveCocoa在View與ViewModel的綁定中至關(guān)重要的同時(shí)各墨,對(duì)應(yīng)用的其他層級(jí)起著巨大的作用。
添加搜索命令
在這個(gè)部分启涯,你會(huì)為validSearchSignal:
方法添加更加實(shí)用的實(shí)現(xiàn)贬堵,用以創(chuàng)建視圖相關(guān)的命令。
打開RWTFlickrSearchViewModel.h
并添加以下導(dǎo)入:
#import <ReactiveCocoa/ReactiveCocoa.h>
和屬性如下:
@property (strong, nonatomic) RACCommand *executeSearch;
RACCommand
是ReactiveCocoa中一個(gè)表示UI操作的概念结洼。它包含UI操作的結(jié)果和當(dāng)前操作是否正在執(zhí)行的狀態(tài)黎做。
在RWTFlickrSearchViewModel.m
的初始化方法中添加代碼如下:
self.executeSearch =
[[RACCommand alloc] initWithEnabled:validSearchSignal
signalBlock:^RACSignal *(id input) {
return [self executeSearchSignal];
}];
這創(chuàng)建了一個(gè)當(dāng)validSearchSignal
發(fā)送true時(shí)有效的命令。
在同一文件的下松忍,添加用于生成命令中執(zhí)行信號(hào)的方法如下:
- (RACSignal *)executeSearchSignal {
return [[[[RACSignal empty]
logAll]
delay:2.0]
logAll];
}
在這個(gè)方法中蒸殿,你將實(shí)現(xiàn)一些在命令執(zhí)行時(shí)處理的業(yè)務(wù)邏輯,并通過信號(hào)異步地返回結(jié)果鸣峭。
上面的方法中暫時(shí)還是假實(shí)現(xiàn)宏所,空的信號(hào)會(huì)立即結(jié)束。延遲操作為所有接受到的next
和completed
事件添加了兩秒的延遲摊溶。這是讓代碼執(zhí)行顯得更為真實(shí)的巧妙方法楣铁。
注意:上面的信號(hào)管道有兩個(gè)附加的
logAll
操作,那是副作用(side effects)的一種更扁,用于打印通過的所有事件盖腕。
這在測(cè)試ReactiveCocoa代碼時(shí)相當(dāng)有用,當(dāng)然如果你認(rèn)為它們并不必要的話浓镜,可以隨時(shí)移除掉這些打印操作溃列。
最后要將這個(gè)命令與視圖關(guān)聯(lián)起來(lái)。打開RWTFlickrSearchViewController.m
并在bindViewModel
方法的末端添加代碼如下:
self.searchButton.rac_command = self.viewModel.executeSearch;
rac_command
是ReactiveCocoa對(duì)UIButton
的擴(kuò)充屬性膛薛。上面的代碼讓按鈕在點(diǎn)擊時(shí)執(zhí)行對(duì)應(yīng)的命令听隐,并把按鈕的可用狀態(tài)與命令的可用狀態(tài)關(guān)聯(lián)起來(lái)。
編譯運(yùn)行哄啄,任意輸入一點(diǎn)文本后點(diǎn)擊“Go”:
你會(huì)看到按鈕只有在文本框中多與3個(gè)字符時(shí)有效雅任。而且,你點(diǎn)擊按鈕時(shí)咨跌,按鈕將在兩秒內(nèi)不可用沪么,直到執(zhí)行信號(hào)完成后才重新變?yōu)榭捎谩?/p>
而在控制臺(tái)中,你還可以看到空信號(hào)立馬結(jié)束锌半,延遲操作讓這事件在延遲兩秒后發(fā)出:
09:31:25.728 RWTFlickrSearch ... name: +empty completed
09:31:27.730 RWTFlickrSearch ... name: [+empty] -delay: 2.0 completed
綁定禽车,綁定和更多的綁定
RACCommand
負(fù)責(zé)了搜索按鈕的狀態(tài)更新,而活動(dòng)指示器(activity indicator)的可見與否則需要你來(lái)處理。
RACCommand
提供了一個(gè)executing
屬性殉摔,這是一個(gè)發(fā)送true或false事件的信號(hào)州胳,用以表明命令是否正在執(zhí)行。你可以在應(yīng)用的其他地方使用這個(gè)屬性反映當(dāng)前命令的狀態(tài)逸月。
在RWTFlickrSearchViewController.m
的bindViewModel
的方法末端添加代碼如下:
RAC([UIApplication sharedApplication], networkActivityIndicatorVisible) =
self.viewModel.executeSearch.executing;
這綁定了UIApplication
的networkActivityIndicatorVisible
屬性和命令的executing
信號(hào)栓撞。這保證了每當(dāng)命令執(zhí)行時(shí),狀態(tài)欄中小型的網(wǎng)絡(luò)活動(dòng)指示器就會(huì)顯示碗硬。
然后繼續(xù)添加代碼如下:
RAC(self.loadingIndicator, hidden) =
[self.viewModel.executeSearch.executing not];
當(dāng)命令執(zhí)行完成時(shí)瓤湘,加載指示符應(yīng)該隱藏起來(lái)。hidden
和executing
在邏輯上是剛好相反的肛响。
很幸運(yùn),ReactiveCocoa為這種情況提供了一個(gè)not方法去翻轉(zhuǎn)信號(hào)的結(jié)果惜索。
最后再添加代碼如下:
[self.viewModel.executeSearch.executionSignals
subscribeNext:^(id x) {
[self.searchTextField resignFirstResponder];
}];
這保證了命令執(zhí)行時(shí)會(huì)把鍵盤隱藏起來(lái)特笋。executionSignals
屬性發(fā)送每次命令執(zhí)行時(shí)生成的信號(hào)(譯注:即代碼中的x
為命令生成的信號(hào))。
這屬性是一個(gè)信號(hào)中的信號(hào)(在早前的教程中介紹過)巾兆。即當(dāng)一個(gè)新的命令執(zhí)行信號(hào)被創(chuàng)建并發(fā)送猎物,鍵盤就會(huì)隱藏。
現(xiàn)在編譯運(yùn)行應(yīng)用角塑,實(shí)際檢驗(yàn)一下上面的代碼蔫磨。
Model在哪?
至今為止你已經(jīng)明確的定義了View(RWTFlickrSearchViewController)和ViewModel(RWTFlickrSearchViewModel)圃伶,但是搀罢,額唧取,Model在哪媒区?
答案非常簡(jiǎn)單:還沒有!
現(xiàn)在應(yīng)用會(huì)在用戶點(diǎn)擊搜索按鈕時(shí)執(zhí)行一個(gè)命令座掘,但具體內(nèi)容還沒實(shí)現(xiàn)。
需要做的是在ViewModel中用當(dāng)前的searchText
屬性搜索Flickr咆霜,并返回一個(gè)匹配圖片的列表脉课。
你可以直接在ViewModel中添加這個(gè)邏輯视事,但相信我,你會(huì)后悔的!如果這是一個(gè)視圖控制器,我賭你一定就這么干了。
ViewModel提供了代表UI狀態(tài)的屬性和代表UI操作的命令(通常是方法)奶卓。其負(fù)責(zé)管理用戶交互導(dǎo)致的UI狀態(tài)變更眉睹。
但是殖卑,ViewModel不應(yīng)該處理因?yàn)榻换?zhí)行的具體的業(yè)務(wù)邏輯接校。那是Model的工作。
在下一步睦柴,你將在應(yīng)用中創(chuàng)建一個(gè)Model層诽凌。里面會(huì)包含一些“腳手架代碼”(scaffolding code),你只要跟著教程做坦敌,你很快就會(huì)發(fā)現(xiàn)更有趣的東西侣诵。
在Model分組下痢法,添加一個(gè)名為RWTFlickrSearch
的協(xié)議,并實(shí)現(xiàn)如下:
#import <ReactiveCocoa/ReactiveCocoa.h>
@import Foundation;
@protocol RWTFlickrSearch <NSObject>
- (RACSignal *)flickrSearchSignal:(NSString *)searchString;
@end
這個(gè)代理定義了Model層的初始接口杜顺,并將搜索Flickr的責(zé)任從ViewModel中分離出來(lái)财搁。
接下來(lái)在同一個(gè)分組中添加一個(gè)NSObject
的子類,名為RWTFlickrSearchImpl
哑舒,并遵守剛添加的協(xié)議:
@import Foundation;
#import "RWTFlickrSearch.h"
@interface RWTFlickrSearchImpl : NSObject <RWTFlickrSearch>
@end
打開RWTFlickrSearchImpl.m
并實(shí)現(xiàn)如下:
@implementation RWTFlickrSearchImpl
- (RACSignal *)flickrSearchSignal:(NSString *)searchString {
return [[[[RACSignal empty]
logAll]
delay:2.0]
logAll];
}
@end
對(duì)此你有沒有感到似曾相識(shí)妇拯?如果有,那是因?yàn)檫@是與先前在ViewModel中相同的假實(shí)現(xiàn)洗鸵。
下一步就要把Model層在ViewModel中用起來(lái)越锈。在ViewModel分組中添加名為RWTViewModelServices
協(xié)議如下:
@import Foundation;
#import "RWTFlickrSearch.h"
@protocol RWTViewModelServices <NSObject>
- (id<RWTFlickrSearch>) getFlickrSearchService;
@end
這協(xié)議定義了一個(gè)方法,用以允許ViewModel獲取一個(gè)遵守RWTFlickrSearch
協(xié)議的實(shí)例引用膘滨。
打開RWTFlickrSearchViewModel.h
并引入這個(gè)新協(xié)議:
#import "RWTViewModelServices.h"
并添加以這為入?yún)⒌某跏蓟椒ǎ?/p>
- (instancetype) initWithServices:(id<RWTViewModelServices>)services;
在RWTFlickrSearchViewModel.m
中甘凭,添加類擴(kuò)展和指向服務(wù)引用的私有屬性:
@interface RWTFlickrSearchViewModel ()
@property (nonatomic, weak) id<RWTViewModelServices> services;
@end
在同一個(gè)文件下,更新初始化方法(譯注:原init
方法)如下:
- (instancetype) initWithServices:(id<RWTViewModelServices>)services {
self = [super init];
if (self) {
_services = services;
[self initialize];
}
return self;
}
這簡(jiǎn)單地存儲(chǔ)了服務(wù)的引用火邓。
最后丹弱,更新executeSearchSignal
方法如下:
Finally, update the executeSearchSignal method as follows.
- (RACSignal *)executeSearchSignal {
return [[self.services getFlickrSearchService]
flickrSearchSignal:self.searchText];
}
以上的方法現(xiàn)在委托Model去進(jìn)行搜索工作。
最后一步就是連接Model和ViewModel铲咨。
在項(xiàng)目的項(xiàng)目根分組中添加一個(gè)命名為RWTViewModelServicesImpl
的NSObject
子類躲胳。打開RWTViewModelServicesImpl.h
并遵守RWTViewModelServices
協(xié)議:
@import Foundation;
#import "RWTViewModelServices.h"
@interface RWTViewModelServicesImpl : NSObject <RWTViewModelServices>
@end
打開RWTViewModelServicesImpl.m
并實(shí)現(xiàn)如下:
#import "RWTViewModelServicesImpl.h"
#import "RWTFlickrSearchImpl.h"
@interface RWTViewModelServicesImpl ()
@property (strong, nonatomic) RWTFlickrSearchImpl *searchService;
@end
@implementation RWTViewModelServicesImpl
- (instancetype)init {
if (self = [super init]) {
_searchService = [RWTFlickrSearchImpl new];
}
return self;
}
- (id<RWTFlickrSearch>)getFlickrSearchService {
return self.searchService;
}
@end
這個(gè)類簡(jiǎn)單地創(chuàng)建了一個(gè)RWTFlickrSearchImpl
實(shí)例,用以搜索Flickr的Model層服務(wù)纤勒,并按需提供給ViewModel坯苹。
最后,打開RWTAppDelegate.m
并添加導(dǎo)入如下:
#import "RWTViewModelServicesImpl.h"
并添加一個(gè)新的私有屬性:
@property (strong, nonatomic) RWTViewModelServicesImpl *viewModelServices;
然后更新createInitialViewController
方法如下:
- (UIViewController *)createInitialViewController {
self.viewModelServices = [RWTViewModelServicesImpl new];
self.viewModel = [[RWTFlickrSearchViewModel alloc]
initWithServices:self.viewModelServices];
return [[RWTFlickrSearchViewController alloc]
initWithViewModel:self.viewModel];
}
編譯運(yùn)行摇天,并確保應(yīng)用像先前一樣運(yùn)行無(wú)誤粹湃。
但這不是這些改變最激動(dòng)人心的地方,看一下這些新代碼的構(gòu)成泉坐。
Model層提供了一個(gè)ViewModel使用的“服務(wù)”为鳄。而一個(gè)協(xié)議定義了這個(gè)服務(wù)的接口,實(shí)現(xiàn)了松耦合腕让。
你可以用這個(gè)方式去為單元測(cè)試提供一個(gè)假的服務(wù)實(shí)例孤钦。應(yīng)用現(xiàn)在已經(jīng)是真正的Model-View-ViewModel結(jié)構(gòu)了。簡(jiǎn)單總結(jié)一下:
- Model層提供服務(wù)并負(fù)責(zé)應(yīng)用的業(yè)務(wù)邏輯纯丸。在本例中偏形,它提供了搜索Flickr的服務(wù)。
- ViewModel代表應(yīng)用的視圖狀態(tài)液南。它同時(shí)也響應(yīng)用戶交互和處理Model層的事件壳猜,并反映在視圖狀態(tài)的改變上。
- View層則非常輕量滑凉,只簡(jiǎn)單提供了ViewModel狀態(tài)的可視化和轉(zhuǎn)發(fā)用戶交互统扳。
注意:在本應(yīng)用中喘帚,Model層使用ReactiveCocoa的信號(hào)提供了服務(wù)。這個(gè)框架強(qiáng)大之處可并不僅僅在于綁定咒钟!
搜索Flickr
在這個(gè)部分吹由,你將要真正實(shí)現(xiàn)Flickr的搜索,是的朱嘴,事情變得越來(lái)越有趣了;]
第一步先創(chuàng)建代表搜索結(jié)果的Model對(duì)象倾鲫。
在Model分組下,添加一個(gè)新的NSObject
子類萍嬉,名為RWTFlickrPhoto
乌昔,并添加3個(gè)屬性如下:
@interface RWTFlickrPhoto : NSObject
@property (strong, nonatomic) NSString *title;
@property (strong, nonatomic) NSURL *url;
@property (strong, nonatomic) NSString *identifier;
@end
這個(gè)Model對(duì)象代表Flickr搜索接口返回的單幅照片數(shù)據(jù)。
打開RWTFlickrPhoto.m
文件并添加describe
方法實(shí)現(xiàn):
- (NSString *)description {
return self.title;
}
這方便你在變更UI之前先在控制臺(tái)測(cè)試打印搜索實(shí)現(xiàn)的結(jié)果壤追。
然后磕道,添加另一個(gè)Model對(duì)象RWTFlickrSearchResults
,同為NSObject
的子類行冰,其中屬性如下:
@import Foundation;
@interface RWTFlickrSearchResults : NSObject
@property (strong, nonatomic) NSString *searchString;
@property (strong, nonatomic) NSArray *photos;
@property (nonatomic) NSUInteger totalResults;
@end
這代表著一個(gè)Flickr搜索返回的照片集合溺蕉。
打開RWTFlickrSearchResults.m
并添加describe
方法實(shí)現(xiàn)(同樣為了方便日志打印):
- (NSString *)description {
return [NSString stringWithFormat:@"searchString=%@, totalresults=%lU, photos=%@",
self.searchString, self.totalResults, self.photos];
}
接下來(lái)是時(shí)候編寫搜索Flickr的代碼了悼做!
打開RWTFlickrSearchImpl.m
并添加導(dǎo)入如下:
#import "RWTFlickrSearchResults.h"
#import "RWTFlickrPhoto.h"
#import <objectiveflickr/ObjectiveFlickr.h>
#import <LinqToObjectiveC/NSArray+LinqExtensions.h>
這導(dǎo)入了你剛創(chuàng)建的Model對(duì)象和兩個(gè)CocoaPods加載的外部引用:
- ObjectiveFlickr:這是基于Objective-C封裝的Flickr接口庫(kù)疯特。它負(fù)責(zé)處理相關(guān)授權(quán)和解析接口返回。相比直接使用Flickr的接口肛走,使用這個(gè)庫(kù)會(huì)更為簡(jiǎn)單漓雅。
- LinqToObjectiveC:這個(gè)庫(kù)提供了一系列流暢的函數(shù)式接口,用于對(duì)數(shù)組和字典進(jìn)行查詢羹与,過濾和轉(zhuǎn)換故硅。
繼續(xù)在RWTFlickrSearchImpl.m
中添加類擴(kuò)展如下:
@interface RWTFlickrSearchImpl () <OFFlickrAPIRequestDelegate>
@property (strong, nonatomic) NSMutableSet *requests;
@property (strong, nonatomic) OFFlickrAPIContext *flickrContext;
@end
這遵守了來(lái)自ObjectiveFlickr
庫(kù)的OFFlickrAPIRequestDelegate
協(xié)議庶灿,并添加了兩個(gè)私有屬性纵搁。你很快就能看到這是如何使用的。
在同一個(gè)文件下添加如下初始化方法如下:
- (instancetype)init {
self = [super init];
if (self) {
NSString *OFSampleAppAPIKey = @"YOUR_API_KEY_GOES_HERE";
NSString *OFSampleAppAPISharedSecret = @"YOUR_SECRET_GOES_HERE";
_flickrContext = [[OFFlickrAPIContext alloc] initWithAPIKey:OFSampleAppAPIKey
sharedSecret:OFSampleAppAPISharedSecret];
_requests = [NSMutableSet new];
}
return self;
}
這創(chuàng)建了一個(gè)Flickr的“上下文”往踢,用以儲(chǔ)存ObjectiveFlickr生成接口請(qǐng)求的必須數(shù)據(jù)腾誉。
注意:要使用ObjectiveFlickr,你要在Flickr App Garden中創(chuàng)建一個(gè)Flickr的應(yīng)用key峻呕。那免費(fèi)而且只有簡(jiǎn)答的幾個(gè)步驟利职。
注意申請(qǐng)的是非商業(yè)用途的key。
ObjectiveFlickr接口相當(dāng)?shù)湫褪莅D銊?chuàng)建接口請(qǐng)求猪贪,結(jié)果的成功與否將通過代理方法返回,具體方法定義在先前提到的OFFlickrAPIRequestDelegate
協(xié)議中讯私。
現(xiàn)有由Model層提供的热押,定義在RWTFlickrSearch
協(xié)議中的接口西傀,只有一個(gè)根據(jù)搜索字符串搜索圖片的方法。
然而桶癣,接下來(lái)你還需要添加更多的方法拥褂。
正因如此,為了優(yōu)化代碼牙寞,你將直接用更加聰明的方式饺鹃,使用信號(hào)改造這些基于代理的接口。
繼續(xù)在RWTFlickrSearchImpl.m
中添加方法如下:
- (RACSignal *)signalFromAPIMethod:(NSString *)method
arguments:(NSDictionary *)args
transform:(id (^)(NSDictionary *response))block {
// 1. Create a signal for this request
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 2. Create a Flick request object
OFFlickrAPIRequest *flickrRequest =
[[OFFlickrAPIRequest alloc] initWithAPIContext:self.flickrContext];
flickrRequest.delegate = self;
[self.requests addObject:flickrRequest];
// 3. Create a signal from the delegate method
RACSignal *successSignal =
[self rac_signalForSelector:@selector(flickrAPIRequest:didCompleteWithResponse:)
fromProtocol:@protocol(OFFlickrAPIRequestDelegate)];
// 4. Handle the response
[[[successSignal
map:^id(RACTuple *tuple) {
return tuple.second;
}]
map:block]
subscribeNext:^(id x) {
[subscriber sendNext:x];
[subscriber sendCompleted];
}];
// 5. Make the request
[flickrRequest callAPIMethodWithGET:method
arguments:args];
// 6. When we are done, remove the reference to this request
return [RACDisposable disposableWithBlock:^{
[self.requests removeObject:flickrRequest];
}];
}];
}
這個(gè)方法根據(jù)方法名和傳遞參數(shù)發(fā)起了一個(gè)接口請(qǐng)求间雀,并使用提供的block參數(shù)轉(zhuǎn)換接口返回悔详。你很快就能看到這是如何運(yùn)作的。
這個(gè)方法相當(dāng)長(zhǎng)惹挟,我們按步驟分析一下:
-
createSignal
方法創(chuàng)建了一個(gè)新信號(hào)伟端。傳遞給block的subscriber參數(shù)用于發(fā)送next, error 和completed事件給信號(hào)的訂閱者。 - 一個(gè)ObjectiveFlickr的請(qǐng)求被構(gòu)建匪煌,這個(gè)請(qǐng)求的引用被保存到requests集合中责蝠。沒有這行代碼,
OFFlickrAPIRequest
將不作保留萎庭! -
rac_signalForSelector:fromProtocol:
方法根據(jù)代理方法創(chuàng)建了一個(gè)信號(hào)霜医,用于表明Flickr接口請(qǐng)求的結(jié)果。 - 訂閱這個(gè)信號(hào)(代理方法生成的)驳规,并轉(zhuǎn)換結(jié)果肴敛,然后作為事件值發(fā)送給第一步創(chuàng)建的信號(hào)(之后我們還會(huì)詳細(xì)談?wù)勥@個(gè))
- 調(diào)用
ObjectiveFlickr
的接口請(qǐng)求。 - 當(dāng)信號(hào)處理完成吗购,這個(gè)block保證了Flickr請(qǐng)求的引用會(huì)被移除医男,避免了內(nèi)存泄漏。
現(xiàn)在我們重點(diǎn)看一下第四部:
[[[successSignal
// 1. Extract the second argument
map:^id(RACTuple *tuple) {
return tuple.second;
}]
// 2. transform the results
map:block]
subscribeNext:^(id x) {
// 3. send the results to the subscribers
[subscriber sendNext:x];
[subscriber sendCompleted];
}];
rac_signalForSelector:fromProtocol:
方法創(chuàng)建了successSignal
信號(hào)捻勉,它同時(shí)也在代理方法的調(diào)用中創(chuàng)建了信號(hào)镀梭。
每當(dāng)代理方法被調(diào)用,一個(gè)next
事件就會(huì)發(fā)送踱启,其中附帶一個(gè)包含方法入?yún)⒌?code>RACTuple报账。而上面的管道則進(jìn)行了如下幾步處理:
- map方法從
flickrAPIRequest:didCompleteWithResponse:
代理方法中提取了它的第二個(gè)NSDictionary
類型的參數(shù)。 - block作為參數(shù)傳遞給第二個(gè)map方法埠偿,用以轉(zhuǎn)換結(jié)果透罢。你接下來(lái)能看到這是如何把字典轉(zhuǎn)換成model對(duì)象的。
- 最后冠蒋,轉(zhuǎn)換的結(jié)果作為
next
事件被發(fā)送羽圃,信號(hào)完成結(jié)束。
注意:這段代碼有一個(gè)小小的問題抖剿,考慮一下朽寞,如果當(dāng)有多個(gè)并發(fā)請(qǐng)求時(shí)會(huì)發(fā)生什么胚吁?在這個(gè)MVVM教程的下半部分你將解決這個(gè)問題,但如果你喜歡挑戰(zhàn)自己的話愁憔,何不現(xiàn)在就嘗試解決一下呢腕扶?
最后一步就是實(shí)現(xiàn)Flickr的搜索方法如下:
- (RACSignal *)flickrSearchSignal:(NSString *)searchString {
return [self signalFromAPIMethod:@"flickr.photos.search"
arguments:@{@"text": searchString,
@"sort": @"interestingness-desc"}
transform:^id(NSDictionary *response) {
RWTFlickrSearchResults *results = [RWTFlickrSearchResults new];
results.searchString = searchString;
results.totalResults = [[response valueForKeyPath:@"photos.total"] integerValue];
NSArray *photos = [response valueForKeyPath:@"photos.photo"];
results.photos = [photos linq_select:^id(NSDictionary *jsonPhoto) {
RWTFlickrPhoto *photo = [RWTFlickrPhoto new];
photo.title = [jsonPhoto objectForKey:@"title"];
photo.identifier = [jsonPhoto objectForKey:@"id"];
photo.url = [self.flickrContext photoSourceURLFromDictionary:jsonPhoto
size:OFFlickrSmallSize];
return photo;
}];
return results;
}];
}
上面的代碼使用了你在上一步添加的signalFromAPIMethod:arguments:transform:
方法。使用flickr.photos.search
接口方法搜索照片吨掌,搜索條件以字典的形式傳入半抱。
作為transform參數(shù)傳入的block把NSDictionary
類型的返回轉(zhuǎn)換成對(duì)等的model對(duì)象,以便于在ViewModel中使用膜宋。
這段代碼使用了LinqToObjectiveC
添加到NSArray
中的linq_select
方法窿侈,以函數(shù)式的接口進(jìn)行數(shù)組轉(zhuǎn)換。
注意:對(duì)于更加復(fù)雜的JSON轉(zhuǎn)對(duì)象操作秋茫,我推薦使用 Mantle史简。盡管在這個(gè)特殊的model中,要正確映射則需要用到它的2.0特性肛著。
最后圆兵,打開RWTFlickrSearchViewModel.m
,并更新搜索信號(hào)枢贿,打印它的搜索結(jié)果殉农。
- (RACSignal *)executeSearchSignal {
return [[[self.services getFlickrSearchService]
flickrSearchSignal:self.searchText]
logAll];
}
編譯運(yùn)行并輸入搜索字符,查看這個(gè)信號(hào)打印在控制臺(tái)的結(jié)果:
2014-06-03 [...] <RACDynamicSignal: 0x8c368a0> name: +createSignal: next: searchString=wibble, totalresults=1973, photos=(
"Wibble, wobble, wibble, wobble",
"unoa-army",
"Day 277: Cheers to the freakin' weekend!",
[...]
"Angry sky",
Nemesis
)
注意:如果你沒有獲取到結(jié)果局荚,那么請(qǐng)?jiān)俅螜z查你的Flickr接口秘鑰和分享密碼(Flickr API key and shared secret)超凳。
至此這個(gè)MVVM教程的上半部分即將結(jié)束,而在結(jié)束之前耀态,當(dāng)前應(yīng)用代碼中還有一個(gè)非常重要的方面你是沒有顧及到的……
內(nèi)存管理
我曾在先前的教程中提到轮傍,在你使用基于block的ReactiveCocoa接口時(shí)要小心避免引用循環(huán),那最終會(huì)導(dǎo)致內(nèi)存的泄漏首装。
如果一個(gè)block中使用了“self”创夜,而“self”又強(qiáng)引用了該block,就會(huì)造成引用循環(huán)簿盅。而在早前的教程中你看到了如何使用@weakify
和@strongify
宏去打破這些引用循環(huán)挥下。
那你有否因?yàn)?code>signalFromAPIMethod:arguments:transform:在引用self時(shí)沒有使用這些宏而感到疑惑揍魂?
那是因?yàn)槟莻€(gè)block是作為參數(shù)傳入到createSignal:
方法中的桨醋,那并不會(huì)在self和block之間構(gòu)成強(qiáng)引用。
很混亂现斋?好吧喜最,你不一定要相信我的話,就直接測(cè)試這代碼去看看有沒內(nèi)存泄漏好了庄蹋。
使用Product / Profile
選項(xiàng)運(yùn)行引用瞬内,選中Allocations配置迷雪。當(dāng)應(yīng)用開始運(yùn)行,使用右上角的過濾器搜索包含“OFF”的類虫蝶,比如ObjectiveFlickr框架中的類章咧。(譯注:在最新的Xcode 8.3.1版本,該過濾輸入框在左下角能真。)
你會(huì)發(fā)現(xiàn)在應(yīng)用啟動(dòng)時(shí)創(chuàng)建了一個(gè)單例OFFlickrAPIContext
赁严。當(dāng)開始搜索時(shí),一個(gè)OFFlickrAPIRequest
的實(shí)例被創(chuàng)建并在搜索期間一直存在:
好消息是粉铐,當(dāng)接口請(qǐng)求結(jié)束疼约,
OFFlickrAPIRequest
實(shí)例就會(huì)被回收。這證明了發(fā)起Flickr請(qǐng)求的block并沒有被持有蝙泼!
我建議你在應(yīng)用開發(fā)中周期性地重復(fù)這項(xiàng)分析操作程剥,以保證內(nèi)存堆中只有你期望的對(duì)象。
何去何從?
這個(gè)示例項(xiàng)目包含了這個(gè)教程至今的所有代碼汤踏。這總結(jié)了這個(gè)MVVM上半部教程的所有內(nèi)容织鲸!
在教程下一部分,你將關(guān)注與如何從ViewModel轉(zhuǎn)換視圖控制器溪胶,和實(shí)現(xiàn)更多的Flickr接口去豐富應(yīng)用的功能昙沦。
譯注:下半部原文不知何種原因錯(cuò)漏較多,故而不發(fā)布翻譯载荔。在第二部分中盾饮,主要涉及的是通過ViewModel層進(jìn)行視圖控制器的跳轉(zhuǎn)。關(guān)于這個(gè)話題懒熙,建議閱讀MVVM With ReactiveCocoa和研究其例子中的源碼丘损。里面的實(shí)現(xiàn)更為全面。