原文鏈接:http://www.sprynthesis.com/2014/12/06/reactivecocoa-mvvm-introduction/
MVC
每一個進行過一段時間軟件開發(fā)的人員都會熟悉MVC。它代表Model View Controller逮刨,是一種在復(fù)雜的軟件應(yīng)用設(shè)計中有效的編碼模式市怎。然而,在iOS開發(fā)中揩瞪,它似乎有第二種含義:Massive View Controller楞艾。它讓許多iOS軟件開發(fā)者糾結(jié)于如何保持代碼整潔清晰逢防,開發(fā)者們意識到,必須為他們的view controller
瘦身摇天。但粹湃,怎么做呢?
MVVM
這就是MVVM出現(xiàn)的原因——它代表Model View View-Model泉坐,它能夠幫我們寫出更加可控再芋,結(jié)構(gòu)更加合理的代碼。
有時坚冀,編寫app的時候不遵守Apple的推薦規(guī)范并不是一個好主意济赎。我并不是不贊成,我只是說记某,這有可能得不償失司训。比如,我不會推薦你去編寫你自己的view controller基類液南,并且自己去管理視圖的生命周期壳猜。
因此,這里我需要回答一個問題:用一個并非Apple推薦的設(shè)計模式(MVC)是否是不明智的呢滑凉?
不统扳!原因有兩點:
- Apple并沒有指導(dǎo)我們?nèi)绾稳ソ鉀QMassive View Controller的問題。它讓我們自己去解決畅姊,去為我們的代碼添加更有效的修改(原注1:今年的WWDC上咒钟,一些Apple的示例代碼中也出現(xiàn)了view model)。MVVM是一個很好的解決的途徑若未。
- MVVM朱嘴,或者說我接下來將要展示的MVVM的編碼模式,是很符合MVC模式規(guī)范的,就好像是我們將MVC向前自然地推進了一步萍嬉。
MVVM的定義
- Model——MVVM中的model的含義并沒有變化乌昔。你的model有可能包含一些業(yè)務(wù)邏輯,這取決于你自己[1]壤追。我傾向于用它來作為一個保存數(shù)據(jù)模型對象的結(jié)構(gòu)[2]磕道,而將創(chuàng)建或管理model的邏輯放到一個單獨的manager類型的類中癌蓖。
-
View——view包含了UI(不管是
UIView
代碼枪孩,storyboard還是xibs),view的邏輯爷耀,以及用戶輸入響應(yīng)资柔。在iOS開發(fā)中焙贷,這其中很多是UIViewController
所做的事情撵割,而不僅僅是UIView
贿堰。 - View-Model——這個詞組本身會使人誤解,雖然它是由兩個我們已經(jīng)了解的詞語構(gòu)成啡彬,但卻代表了完全不同的一種東西羹与。它不是傳統(tǒng)數(shù)據(jù)模型結(jié)構(gòu)意義上的model(再次表明,這也只是我的個人傾向)庶灿。它的職能之一是作為一個靜態(tài)模型纵搁,代表view展示所需要的數(shù)據(jù)。但此外往踢,它也負責收集腾誉、解釋、轉(zhuǎn)換這些數(shù)據(jù)峻呕。這就使得view (controller)有了更清晰專一的職責:將view-model提供的數(shù)據(jù)展示出來利职。
更多關(guān)于view-model
view-model這個詞組的確不能表達我們的意思。一個更好的表述應(yīng)當是"view coordinator(視圖協(xié)調(diào)器)"瘦癌。你可以把它想象成電視新聞節(jié)目幕后的調(diào)查者和撰稿人猪贪。它從信息源搜集原始數(shù)據(jù)(可能是數(shù)據(jù)庫,網(wǎng)絡(luò)協(xié)議讯私,等等)热押,進行邏輯演繹,將數(shù)據(jù)轉(zhuǎn)換成view (controller)可展示的數(shù)據(jù)斤寇。它僅僅暴露(往往通過屬性property
)view controller展示view所必需的信息(理想情況下你不應(yīng)該暴露你的數(shù)據(jù)模型對象)桶癣。它也負責對上游數(shù)據(jù)進行修改(例如,更新model/數(shù)據(jù)庫娘锁,POST數(shù)據(jù)等等)鬼廓。
MVVM in a MVC world
正如詞組view-model一樣,我覺得MVVM這個縮寫詞一定程度上也不能清楚代表我們在iOS開發(fā)中的使用方法致盟。讓我們再看一下這個縮寫詞碎税,看看它是怎么融入MVC的尤慰。
為了畫出示意圖,讓我們把MVC中的V和C調(diào)換一下雷蹂,這樣得到的縮寫詞伟端,MCV,能夠更準確反映各個部分之間的關(guān)系匪煌。對于MVVM责蝠,我們采取一樣的做法,把V(View)移動到VM的右邊萎庭,得到MVMV(我確定最初不采取這種更直觀的命名是有原因的)霜医。
下圖描述了這兩種模式在iOS中是如何融為一體的:
- 我盡量將各個方塊的大小與各部分所承擔的任務(wù)量對應(yīng)起來。
- 注意到驳规,view controller的方塊有多大肴敛!
- 可以看到,龐大的view controller與view-model之間有很大一部分工作是重疊的吗购。
- 你也可以看到医男,view controller的一部分與MVVM中的view是重合的。
你也許會感到寬慰的是:實際上我們并沒有拋棄view controller的概念捻勉。我們只是將其中那一大塊重疊的部分放進view-model中镀梭,讓view controller更輕松。
最終踱启,我們得到的其實是MVMCV报账,Model View-Model Controller View。
我們得到的結(jié)果是:
現(xiàn)在埠偿,view controller唯一要做的就是用來自view-model的數(shù)據(jù)來調(diào)度透罢,管理不同的view,并在用戶輸入需要改變上游數(shù)據(jù)的時候告訴view-model胚想。view controller并不需要知道網(wǎng)絡(luò)請求琐凭,core data,model對象(原注3.1:但實踐中浊服,有時候通過view-model的頭文件來暴露一些model是很有效的方法统屈,而不是再去復(fù)制大量的屬性,稍后詳談)[3]牙躺,等等愁憔。
view-model將作為view controller的屬性
property
存在。view controller了解view-model及其公共屬性孽拷,但view-model對view controller毫不知情吨掌。你應(yīng)該已經(jīng)感覺到這種分離的好處了。另一種幫助你理解這些組成部分之間的關(guān)系,以及各部分的職能的方法就是膜宋,看下面這張新的應(yīng)用層級結(jié)構(gòu)圖:
View-Model和View Controller:和而不同
讓我們看一個簡單的view-model頭文件來更深入了解我們的新模式長啥樣窿侈。簡單起見,讓我們編寫一個假冒的twitter客戶端秋茫,它能讓我們查找任何twitter用戶最近的回復(fù)史简,只需要輸入用戶名,然后點擊"Go"肛著。我們的界面長這樣:
- 有一個
UITextField
用來輸入用戶名圆兵,一個“Go”按鈕UIButton
- 有一個
UIImageView
和一個UILable
,展示當前查找的用戶的頭像和名字枢贿。 - 下方有一個
UITableView
用來展示最近的回復(fù)(推特)殉农。 - 可以無限滾動。
示例View-Model
我們view-model的頭文件可能會長這樣:
@interface MYTwitterLookupViewModel: NSObject
@property (nonatomic, assign, readonly, getter=isUsernameValid) BOOL usernameValid;
@property (nonatomic, strong, readonly) NSString *userFullName;
@property (nonatomic, strong, readonly) UIImage *userAvatarImage;
@property (nonatomic, strong, readonly) NSArray *tweets;
@property (nonatomic, assign, readonly) BOOL allTweetsLoaded;
@property (nonatomic, strong, readwrite) NSString *username;
- (void) getTweetsForCurrentUsername;
- (void) loadMoreTweets;
十分明了清晰局荚。注意到這些壯觀的readonly
屬性了嗎超凳?View-model會暴露盡量少的信息給view controller,view controller也不關(guān)心view-model是怎么獲得這些信息的(現(xiàn)在我們也不需要關(guān)心危队,只需要想像成我們常用的網(wǎng)絡(luò)請求得到的數(shù)據(jù)聪建,偽造的數(shù)據(jù)钙畔,持久化存儲的數(shù)據(jù)等等)茫陆。
view-model不會做的事情:
- 以任何方式直接操作view controller,或有變化時直接通知到view controller擎析。
View controller
View controller會使用從view-model獲得的數(shù)據(jù)來:
- 根據(jù)
usernameValid
屬性的變化來改變“GO”按鈕的enabled
屬性 - 當
usernameValid
為NO時將按鈕的alpha
值設(shè)為.5(當usernameValid
為YES時為1.0) - 用
userFullName
里的字符串來更新UILabel的text
屬性 - 使用
userAvatarImage
的值來更新UIImageView的Image
- 使用
tweets
里的對象來設(shè)置tableview cell - 當tableview 滑到底時簿盅,如果
allTweetsLoaded
的值為NO,需要添加一個"loading" cell
View controller可能以下列方式來操作view-model:
- 當UITextField里的text改變時揍魂,更新我們view-model中唯一的
readwrite
屬性桨醋,username
- 當用戶點擊"Go"按鈕時,調(diào)用view-model的
getTweetsForCurrentUsername
方法 - 當tableview 滑動到“l(fā)oading cell”時现斋,調(diào)用view-model的
loadMoreTweets
方法
View controller不做的事情:
- 發(fā)起網(wǎng)絡(luò)請求
- 管理
tweets
數(shù)組 - 判斷username是否是有效名字
- 將用戶的first name和last name拼成full name
- 下載用戶頭像喜最,并將其轉(zhuǎn)換成UIImage(原注4:如果你習慣使用一些UIImageView的category來加載網(wǎng)絡(luò)圖片的話,你可以不暴露一個UIImage庄蹋,而是暴露一個URL瞬内,這確實能夠?qū)iew-model和UIKit分隔得更清楚。但就我自己而言限书,我更將UIIMage看作一種數(shù)據(jù)虫蝶,而不是用來表現(xiàn)數(shù)據(jù)的視圖。這里并沒有明顯的界線)
- 做許多費力的活兒
再一次倦西,注意到對于view-model的變化的作出響應(yīng)的責任在view controller中能真。
Child View-Model
之前提到,使用tweets
里的對象來設(shè)置tableview cell。通常你希望這些對象是代表一條條推特的數(shù)據(jù)模型對象粉铐。你可能會感到疑惑:誒不是說MVVM中我們盡量不暴露數(shù)據(jù)模型的嗎疼约?(原注3)
并不是一個view-model代表了屏幕上所有的東西。我們可以使用child view-model來代表屏幕上更小的蝙泼,更模塊化的元素忆谓。當這種元素可以被重用(比如tableview cell),或者代表了多個data-model對象的時候踱承,這種方法尤其有效倡缠。
我們并不總是需要child view-model。例如茎活,我們可以使用一個table header view來實現(xiàn)我們的“tweetboat plus” APP的頂部昙沦。這部分是不可重用的,所以我們可以直接傳我們在view controller中所使用的view-model到這個自定義的header view载荔。Header view從view-model中挑選自己需要的信息盾饮,忽略其他的信息。這也很有利于保持各個subview的同步懒熙,因為它們都是使用的同一套信息丘损,并監(jiān)聽同樣的屬性變化。
在我們的demo中工扎,tweets
數(shù)組會裝滿child view-model徘钥,child view-model也許長這樣:
@interface MYTweetCellViewModel: NSObject
@property (nonatomic, strong, readonly) NSString *tweetAuthorFullName;
@property (nonatomic, strong, readonly) UIImage *tweetAuthorAvatarImage;
@property (nonatomic, strong, readonly) NSString *tweetContent;
你可能會覺得,這長得很像我們正常使用的“Tweet”數(shù)據(jù)模型啊肢娘。那為什么要把它轉(zhuǎn)換成一個view-model呢呈础?因為即使很相似,view-model能讓我們將對外暴露的信息盡量壓縮到我們需要的范圍橱健,并提供一些可能是經(jīng)過加工的屬性而钞,或者計算一些為這個視圖所獨有的數(shù)據(jù)。(再次聲明拘荡,盡量不暴露可變的數(shù)據(jù)模型對象臼节,因為我們希望是由view-model自身來改變這些數(shù)據(jù)模型對象,而不是view或view controller)
那珊皿,在哪里創(chuàng)建view-model呢
我們的View-Model會在何時何地被創(chuàng)建呢网缝?是由view controller自己創(chuàng)建自己的view-model嗎?
View-Model 創(chuàng)造 View-Model
嚴格來講亮隙,你應(yīng)當在app delegate中為你的top view controller創(chuàng)建一個view-model途凫。當present一個新的view controller,或者一個新的由view-model代表的view的時候溢吻,你請求當前的view-model來創(chuàng)建一個child view-model维费。
例如果元,我們想增加一個資料頁(profile view controller),用以當用戶點擊APP頂部區(qū)域中的頭像的時候跳轉(zhuǎn)犀盟。我們可以為我們的主view-model增加一個方法:
- (MYTwitterUserProfileViewModel *) viewModelForCurrentUser;
在我們的主view controller中這樣調(diào)用它:
- (IBAction) didTapPrimaryUserAvatar
{
MYTwitterUserProfileViewModel *userProfileViewModel = [self.viewModel viewModelForCurrentUser];
MYTwitterUserProfileViewController *profileViewController =
[[MYTwitterUserProfileViewController alloc] initWithViewModel: userProfileViewModel];
[self.navigationController pushViewController: profileViewController animated:YES];
}
在這個例子中而晒,我想要present一個當前用戶的profile view controller,但這個profile view controller需要一個view-model阅畴。我們這里的view controller并不知道創(chuàng)建這個profile view controller所需要的全部信息(當然倡怎,它也不應(yīng)該知道),因此贱枣,view controller會讓自己的view-model來做這個工作监署。
View-Models 列表
在這個demo中,當我們收到數(shù)據(jù)的時候(比如可能是通過一個網(wǎng)絡(luò)請求)我們會提前創(chuàng)建好所有cell對應(yīng)的child view-model纽哥。在這種情況下钠乏,主view-model中的tweets
數(shù)組中會裝滿了MYTweetCellViewModel
對象。在我們的tableview的cellForRowAtIndexPath
中春塌,我們只需要找到index對應(yīng)的那個view-model晓避,然后把它賦給cell。
Functional Core, Imperative Shell (函數(shù)式內(nèi)核只壳,命令式外殼)
這種view-model的軟件設(shè)計方式可以看成一種近似于由Gary Bernhardt所提出的 “Functional Core, Imperative Shell”軟件設(shè)計模式
Functional Core(函數(shù)式內(nèi)核)
View-Model 是我們的"函數(shù)式內(nèi)核"俏拱,盡管在iOS/OC中,很難達到純函數(shù)式的程度(Swift給我們提供了更多的函數(shù)性)吼句。通常的思想是锅必,讓我們的view-models盡可能少地依賴于/影響到應(yīng)用的其它部分。什么意思呢命辖?回想一下剛開始學習編程的時候?qū)戇^的最簡單的函數(shù)况毅。它們可能會接受1到2個參數(shù)分蓖,然后輸出一個結(jié)果尔艇。Data in, Data out。函數(shù)中或許進行了一些簡單的計算么鹤,或者諸如拼合first name和last name之類终娃。不管程序其它部分怎么運作,相同的輸入總是會產(chǎn)生相同的輸出蒸甜。這就是函數(shù)的思想棠耕。
這正是我們使用view-models所要取得的結(jié)果。View-model內(nèi)部包含了轉(zhuǎn)換數(shù)據(jù)的邏輯柠新,并將結(jié)果作為property保存下來窍荧。理想情況下,相同的輸入(例如網(wǎng)絡(luò)響應(yīng))總會產(chǎn)生相同的輸出(property的值)恨憎。這就是說蕊退,會盡可能消除外部對于結(jié)果的影響郊楣,比如 使用大量的狀態(tài)值。我們要做的第一步就是在你的view-model的頭文件中不要包含UIKit.h瓤荔。(原注6:這是一個很好的原則净蚤,但也有一些灰色區(qū)域:比如,你可能會將UIImage看作數(shù)據(jù)输硝,而不是視圖(我喜歡這樣)今瀑。在這種情況下,你需要UIKit.h來獲得UIImage類)UIKit天生就會影響到APP的很多地方点把,它包含了許多副作用橘荠,因此改變一個值,或者調(diào)用某個方法都可能會產(chǎn)生不直接的變化郎逃。
更新:剛剛看了Andy在Functional Swift Conference上的另一個很棒的演講砾医,因此對此有了更多的思考。我們的view-model說到底還是一個對象衣厘,還是需要保持一些狀態(tài)變量(否則不會成為一個很實用的對象)如蚜。但我們?nèi)匀恍枰驯M可能多的邏輯寫到無狀態(tài)的函數(shù)中。在這方面Swift又一次做的比OC更好影暴。
Imperative (Declarative?) Shell(命令式(聲明式错邦?)外殼)[4]
我們將view-model數(shù)據(jù)轉(zhuǎn)換成屏幕所顯示的東西,需要做一系列工作型宙,比如所有的狀態(tài)改變撬呢,應(yīng)用內(nèi)其它部分的改變,命令式外殼就是我們做這些臟活兒累活兒的地方妆兑。這就是我們的view (controller)魂拦,我們處理UIKit的地方。我依然特別注意盡可能的減少狀態(tài)變量搁嗓,將這一系列工作用聲明式的方式完成芯勘,例如使用ReactiveCocoa。但本質(zhì)上腺逛,iOS和UIKit是命令式的荷愕。(原注7:table data source是一個很好的示例:它的這種代理模式會使得delegate使用狀態(tài)變量來在tableview請求數(shù)據(jù)的時候提供信息。事實上棍矛,一般情況下代理模式都會使用大量的狀態(tài)變量)
可測試的內(nèi)核
iOS中的單元測試是一項糟糕的安疗,變態(tài)的,令人討厭的工作(够委。荐类。。)至少這是我開始接觸做這些時的感想茁帽。我甚至讀過一兩本這方面的書玉罐,但當他們開始偽造(mocking)真竖,偷換(swizzling) view controllers來使得其中一些邏輯可以測試的時候,我就開始打瞌睡厌小。最終恢共,我選擇了退而求其次,只對models和一些相關(guān)的model manager類進行單元測試璧亚。
除了因為減少狀態(tài)變量而減少的bug之外讨韭,View-model這種函數(shù)式內(nèi)核最大的優(yōu)點之一就是,它能夠很好地支持單元測試癣蟋。如果對于一些方法透硝,相同的輸入總會產(chǎn)生相同的輸出,那么這些方法就很適合做單元測試疯搅。我們現(xiàn)在將數(shù)據(jù)的收集/邏輯/轉(zhuǎn)化濒生,與復(fù)雜的view controller分離開了,這就意味著不需要偽造幔欧,偷換等等瘋狂的舉動就可以寫出很好的測試了罪治。
連接一切
那么,當view-model中的公共屬性發(fā)生變化的時候礁蔗,我們怎么去更新view呢觉义?
大部分時候,我們會用相應(yīng)的view-model來初始化view controller浴井,正如我們上面所看到的:
MYTwitterUserProfileViewController *profileViewController = [[MYTwitterUserProfileViewController alloc] initWithViewModel: userProfileViewModel];
有時晒骇,在初始化的時候不能傳入view-model,比如使用storyboard segue或者cell重用的時候磺浙。這時洪囤,可以讓view (controller)對外暴露一個readwrite的view-model屬性。
MYTwitterUserCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"MYTwitterUserCell" forIndexPath:indexPath];// grab the cell view-model from the vc view-model and assign it
cell.viewModel = self.viewModel.tweets[indexPath.row];
若我們可以在init
或者viewDidLoad
之前傳入view-model的話撕氧,我們就可以利用view-model的屬性來初始化一些UI元素的狀態(tài)瘤缩。
- (id) initWithViewModel:(MYTwitterLookupViewModel *) viewModel {
self = [super init];
if (!self) return nil;
_viewModel = viewModel;
return self;
}
- (void) viewDidLoad {
[super viewDidLoad];
_goButton.enabled = viewModel.isUsernameValid;
_goButton.alpha = viewModel.isUsernameValid ? 1 : 0.5;
// etc
}
太棒了!我們搞定了我們的初始化呵曹。但若view-model中的值發(fā)生變化呢款咖?GO按鈕怎么才能變成enabled的呢?我們的user label和頭像怎么才能利用網(wǎng)絡(luò)請求響應(yīng)來填充呢奄喂?
我們可以將view controller暴露給view-model,這樣view-model在相關(guān)數(shù)據(jù)發(fā)生變化的時候海洼,就能調(diào)用view controller的“updateUI”方法(千萬別這么做我說著玩的)跨新。將view controller作為view-model的一個delegate?當view-model中屬性發(fā)生變化的時候拋出一個通知坏逢?Noooooooooo域帐!
我們的view controller確實會知道view-model中的一些變化赘被。我們可以利用UITextField
代理方法,每當text發(fā)生變化的時候肖揣,檢查view-model來更新按鈕的狀態(tài)民假。
- (void)textFieldDidChange:(UITextField *)sender {
// update the view-model
self.viewModel.username = sender.text;
// check if things are now valid
self.goButton.enabled = self.viewModel.isUsernameValid;
self.goButton.alpha = self.viewModel.isUsernameValid ? 1.0 : 0.5;
}
如果UITextField的text變化是導(dǎo)致view-model的isUsernameValid
屬性變化的唯一原因的話,這樣確實可以解決問題龙优。但如果還有其它變量/方法會改變isUsernameValid
的值呢羊异?如果view-model內(nèi)部的網(wǎng)絡(luò)請求會改變這個值呢?也許我們可以為view-model中的方法添加completion handlers來更新UI彤断?或許我們可以使用鄭重其事野舶,繁瑣笨重的KVO?
也許宰衙,不管怎么樣我們最終都可以利用我們熟悉的種種機制將view-model和view controller的接觸點連接起來平道。但你已經(jīng)知道了,這不是我們寫這篇文章的原因供炼。這些方法都為我們的代碼增加了大量分散的修改UI邏輯的入口一屋。
進入ReactiveCocoa的世界
ReactiveCocoa (RAC)為我們提供了一種清楚的解決方案。現(xiàn)在讓我們看看吧袋哼!
若有一個表單陆淀,當表單判斷為有效時,更新一個提交按鈕的狀態(tài)先嬉≡唬考慮如何控制這些信息的流動。現(xiàn)在的你可能是這么做的:
最終疫蔓,你小心地使用這些狀態(tài)變量含懊,在代碼里各個不相關(guān)的地方處理這個簡單的邏輯⌒普停看到這個信息流的許多不同入口了嗎岔乔?(這還只是一個UI元素的一套邏輯而已!)我們所使用的這種抽象并不聰明滚躯,不能為我們追蹤這些事情之間的關(guān)系雏门,所以我們只好可憐兮兮地自己來做這些事情。
讓我們來看看聲明式的做法
這看起來有點像以前學校里CS課上使用的APPLICATION流程圖掸掏。利用聲明式編程方法茁影,我們使用一種更高的抽象,使得我們編程的方法更接近我們腦海中設(shè)計應(yīng)用流程圖的過程丧凤。我們把更多的事情丟給計算機去做募闲。實際的代碼現(xiàn)在更接近這張圖了。
RACSignal
RACSignal
(signal愿待,信號量)是RAC所有內(nèi)容的基石浩螺。它是一個代表我們最終會收到的信息的對象靴患。當你能夠?qū)⒃谖磥砟硞€時刻接收到的信息用一個具體的對象來表示的時候,你就可以提前寫好所有的邏輯要出,并提前建立起完整的信息流(聲明式)鸳君,而不是等那個事件發(fā)生的時候再去做這些(命令式)。
一個信號量將app中所有控制這個信息流動的異步的方法(delegates, callback blocks, notifications, KVO, target/action event observers, 等等)整合在一個地方患蹂。這是很有意義的一件事或颊。此外,它還能讓你在該信息流動的過程中輕松地轉(zhuǎn)換/分離/整合/過濾信息况脆。
那么饭宾,什么是signal信號量呢?這是一個信號量:
一個信號量是一個會輸出一連串值流的對象格了。但此處的這個信號量并沒有任何作用看铆,因為它沒有任何訂閱者(subscriber)。一個信號量只有在擁有訂閱者聆聽它的時候才會傳出信息盛末。信號量會向它的訂閱者傳出大于等于0個"next"事件弹惦,其中包含了所需要的值。隨后悄但,它會再傳出一個"complete"事件或是一個"error"事件棠隐。一個信號量有點類似于其它語言或框架中的"promise",但信號量的作用更強大檐嚣,不僅僅是只傳遞一次返回值而已助泽。
上文提過,我們可以根據(jù)需要對信號量傳遞出的值進行過濾嚎京,轉(zhuǎn)換嗡贺,分離,整合等鞍帝。不同的訂閱者可能會以不同的方式來使用信號量傳出的這些值诫睬。
信號量是從哪兒獲得它們所傳遞的這些值的呢?
信號量是一段異步的代碼帕涌,等待某些事情發(fā)生之后摄凡,將結(jié)果值傳給它的訂閱者。你可以使用RACSignal
類的類方法createSignal:
手動創(chuàng)建一個信號量:
RACSignal *networkSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NetworkOperation *operation = [NetworkOperation getJSONOperationForURL:@"http://someurl"];
[operation setCompletionBlockWithSuccess:^(NetworkOperation *theOperation, id *result) {
[subscriber sendNext:result];
[subscriber sendCompleted];
} failure:^(NetworkOperation *theOperation, NSError *error) {
[subscriber sendError:error];
}];
這里我在一個(假的)帶有success block 和 failure block的網(wǎng)絡(luò)請求操作中創(chuàng)建了一個信號量(原注8:如果想等到有訂閱者的時候才真正地發(fā)起網(wǎng)絡(luò)請求蚓曼,可以使用RACSignal
類的類方法defer
)亲澡。在success block中,我對參數(shù)subscriber
對象調(diào)用了sendNext:
方法和sendCompleted
方法辟躏,在failure block中谷扣,則調(diào)用了sendError:
方法。現(xiàn)在捎琐,我可以訂閱這個信號量了会涎,每當有網(wǎng)絡(luò)請求響應(yīng)返回的時候,我都能收到一個json值或是一個error瑞凑。
幸運的是末秃,RAC框架的創(chuàng)造者們在實際項目中也運用著該框架(我猜的),因此籽御,他們很清楚我們的代碼中最需要的是什么练慕。他們提供給我們一套很好的機制,來將我們以前習慣使用的異步模式轉(zhuǎn)換成信號量技掏。只是铃将,萬一你有一個異步的任務(wù),用內(nèi)置的信號量類型無法完成的時候哑梳,不要忘了還可以使用createSignal:
或類似方法方便地自己創(chuàng)建一個劲阎。
他們所提供的機制之一就是RACObserve()
宏(如果你不喜歡用宏,你可以很容易地使用更底層的方法鸠真。這會稍微繁瑣點兒悯仙,但依舊很好。對于swift吠卷,這里也有一份教程 using the RAC library with swift锡垄,以及swift版本 swifty replacement)。RACObserver()
宏是RAC對復(fù)雜的KVO的替代方案祭隔。你只需要將所觀察的對象以及想要觀察的該對象的屬性的keypath作為參數(shù)傳入货岭,RACObserver
會生成一個信號量,并立刻將該屬性當前的值傳出(如果有訂閱者的話)疾渴,并且將來該屬性的值變化的話千贯,該變化值也會由此傳出。
RACSignal *usernameValidSignal = RACObserve(self.viewModel, usernameIsValid);
這僅僅是RAC所提供的一種創(chuàng)建信號量的方式。還有其它多種創(chuàng)建信號量的方式:
RACSignal *controlUpdate = [myButton rac_signalForControlEvents:UIControlEventTouchUpInside];
// signals for UIControl events send the control event value (UITextField, UIButton, UISlider, etc)
// subscribeNext:^(UIButton *button) { NSLog(@"%@", button); // UIButton instance }
RACSignal *textChange = [myTextField rac_textSignal];
// some special methods are provided for commonly needed control event values off certain controls
// subscribeNext:^(UITextField *textfield) { NSLog(@"%@", textfield.text); // "Hello!" }
RACSignal *alertButtonClicked = [myAlertView rac_buttonClickedSignal];
// signals for some delegate methods send the delegate params as the value
// e.g. UIAlertView, UIActionSheet, UIImagePickerControl, etc
// (limited to methods that return void)
// subscribeNext:^(NSNumber *buttonIndex) { NSLog(@"%@", buttonIndex); // "1" }
RACSignal *viewAppeared = [self rac_signalForSelector:@selector(viewDidAppear:)];
// signals for arbitrary selectors that return void, send the method params as the value
// works for built in or your own methods
// subscribeNext:^(NSNumber *animated) { NSLog(@"viewDidAppear %@", animated); // "viewDidAppear 1" }
要記住,你也可以輕松創(chuàng)建自己的信號量抵代,包括替換其他未被支持的代理模式墨坚。想象一下吧!我們現(xiàn)在能從這所有不相關(guān)的信息流動的異步事件中抽取出信號量竹祷,并將它們?nèi)掀饋恚】幔∷鼈儠蔀樯衔穆暶魇搅鞒虉D中的節(jié)點申尼。這是多么讓人雞凍啊垫桂!
什么是訂閱者(subscriber)师幕?
簡單來說,一個訂閱者就是一段代碼,它等待信號量傳來的值霹粥,并用這些值來做一些事情(當然灭将,也可以用“complete”和"error"來做一些事情)。
這里后控,通過將一個block作為參數(shù)傳到一個信號量的subscribeNext
實例方法中庙曙,我們就創(chuàng)建了一個簡單的訂閱者。此時浩淘,我們通過RACObserve()
宏創(chuàng)建了一個信號量捌朴,并以此來監(jiān)聽一個對象的一個屬性,并將其值賦給本身的一個屬性张抄。
- (void) viewDidLoad {
// ...
// create and get a reference to the signal
RACSignal *usernameValidSignal = RACObserve(self.viewModel, isUsernameValid);
// update the local property when this value changes
[usernameValidSignal subscribeNext:^(NSNumber *isValidNumber) {
self.usernameIsValid = isValidNumber.boolValue
}];
}
注意到砂蔽,RAC處理的都是對象,不是如BOOL
的原始值類型署惯。不過不用擔心左驾,RAC大部分情況下都會為你自動轉(zhuǎn)換好。
更棒的是泽台,RAC的創(chuàng)造者認為什荣,像這種將一個屬性的值綁定到另一個屬性上,并監(jiān)聽其變化的行為是一種很常見的需求怀酷,因此他們提供了另一個宏RAC()
稻爬。類似于RACObserve()
,你只要傳入監(jiān)聽的對象蜕依,以及你想要綁定到的參數(shù)桅锄,剩下的工作(創(chuàng)建一個訂閱者,更新參數(shù)等)就交給底層去做吧样眠!這樣友瘤,我們上面的例子就變成了這樣:
- (void) viewDidLoad {
//...
RAC(self, usernameIsValid) = RACObserve(self.viewModel, isUsernameValid)
;}
但這里,我們的目的并不在于此檐束。我們并不想用另一個屬性來保存信號量傳過來的值(因為這樣又會產(chǎn)生狀態(tài)變量了)辫秧。我們真正想做的事情是利用信號量傳過來的信息來更新UI。
轉(zhuǎn)化接收到的值流
現(xiàn)在我們來看看被丧,RAC提供給我們什么方法來轉(zhuǎn)化接收到的值流的盟戏。這里我們要使用的是RACSignal
的實例方法map
。
- (void) viewDidLoad {
//...
RACSignal *usernameIsValidSignal = RACObserve(self.viewModel, isUsernameValid);
RAC(self.goButton, enabled) = usernameIsValidSignal;
RAC(self.goButton, alpha) = [usernameIsValidSignal
map:^id(NSNumber *usernameIsValid) {
return usernameIsValid.boolValue ? @1.0 : @0.5;
}];
}
現(xiàn)在甥桂,我們將view-model的isUsernameValid
屬性的變化直接與goButton按鈕的enabled
綁定起來啦柿究!這是多棒!與alpha
值的綁定就更酷了黄选,我們將BOOL值通過map
方法轉(zhuǎn)化成了alpha
屬性的數(shù)據(jù)類型(注意我們這里返回的是一個NSNumber
類型蝇摸,而不是普通數(shù)據(jù)類型)。
多個訂閱者,副作用貌夕,以及復(fù)雜的操作
當訂閱一個信號量鏈的時候律歼,有一點很重要的事情要注意:每次有一個新的值通過信號量鏈[5]傳遞的時候,每有一個訂閱者蜂嗽,值就會被傳遞一次苗膝,這些值不會在任何地方被存儲起來(除了RAC內(nèi)部實現(xiàn)的時候)殃恒。當一個信號量想傳遞出去一個新值的時候植旧,它會遍歷其所有訂閱者,對每一個訂閱者都傳一次离唐。(這是信號量鏈的一個簡化了的解釋病附,但基本思想是對的)
了解這點為什么重要呢?這意味著亥鬓,在這一信號量鏈中某處產(chǎn)生的副作用完沪,任何影響應(yīng)用的轉(zhuǎn)換,都會重復(fù)發(fā)生多次嵌戈。這通常是剛接觸RAC的使用者所不希望的覆积。(這也違背了函數(shù)式data in, data out的原則)
舉一個略顯刻意的例子:有一個按鈕點擊事件的信號量,在信號量鏈中的某處熟呛,會增加self
的counter屬性宽档。如果有多個訂閱者訂閱了這個信號量鏈,counter屬性會比你預(yù)期的增加得更多庵朝。你必須盡量消除信號量鏈中產(chǎn)生的副作用吗冤。如果實在不能避免中間的副作用,你也可以使用一些方法來防止副作用的影響[6]九府。我將在另一篇文章中講解椎瘟。
除了副作用,你也要當心那些包含耗時操作或可變數(shù)據(jù)的信號量鏈侄旬。網(wǎng)絡(luò)請求包含了以上所說的三個注意點:
- 網(wǎng)絡(luò)請求影響了你app的網(wǎng)絡(luò)層(副作用)
- 網(wǎng)絡(luò)請求為你的信號量鏈引入了可變數(shù)據(jù)(兩個完全相同的網(wǎng)絡(luò)請求可能會返回不同的數(shù)據(jù))
- 網(wǎng)絡(luò)請求速度較慢肺蔚。
舉例:我們有一個信號量,每當一個按鈕被點擊的時候儡羔,會傳出一個值宣羊。我們想將這個值通過網(wǎng)絡(luò)請求轉(zhuǎn)換為另一個結(jié)果。如果這個信號量鏈有多個訂閱者要使用最后的值笔链,這中間就會產(chǎn)生多次的網(wǎng)絡(luò)請求段只。
顯然,網(wǎng)絡(luò)請求是一個很常見的需求鉴扫。正如你預(yù)料赞枕,RAC為這些情況提供了解決方案,即RACCommand
和multicasting。我將在我的下篇文章中詳細講解炕婶。
Tweetboat Plus
好了姐赡,經(jīng)過了簡單的介紹(哈?)柠掂,讓我們看一下项滑,如何利用ReactiveCocoa連接我們的view model和view controller。
//
// View Controller
//
- (void) viewDidLoad {
[super viewDidLoad];
RAC(self.viewModel, username) = [myTextfield rac_textSignal];
RACSignal *usernameIsValidSignal = RACObserve(self.viewModel, usernameValid);
RAC(self.goButton, alpha) = [usernameIsValidSignal
map: ^(NSNumber *valid) {
return valid.boolValue ? @1 : @0.5;
}];
RAC(self.goButton, enabled) = usernameIsValidSignal;
RAC(self.avatarImageView, image) = RACObserve(self.viewModel, userAvatarImage);
RAC(self.userNameLabel, text) = RACObserve(self.viewModel, userFullName);
@weakify(self);
[[[RACSignal merge:@[RACObserve(self.viewModel, tweets),
RACObserve(self.viewModel, allTweetsLoaded)]]
bufferWithTime:0 onScheduler:[RACScheduler mainThreadScheduler]]
subscribeNext:^(id value) {
@strongify(self);
[self.tableView reloadData];
}];
[[self.goButton rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext: ^(id value) {
@strongify(self);
[self.viewModel getTweetsForCurrentUsername];
}];
}
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// if table section is the tweets section
if (indexPath.section == 0) {
MYTwitterUserCell *cell =
[self.tableView dequeueReusableCellWithIdentifier:@"MYTwitterUserCell" forIndexPath:indexPath];
// grab the cell view model from the vc view model and assign it
cell.viewModel = self.viewModel.tweets[indexPath.row];
return cell;
} else {
// else if the section is our loading cell
MYLoadingCell *cell =
[self.tableView dequeueReusableCellWithIdentifier:@"MYLoadingCell" forIndexPath:indexPath];
[self.viewModel loadMoreTweets];
return cell;
}
}
//
// MYTwitterUserCell
//
// this could also be in cell init
- (void) awakeFromNib {
[super awakeFromNib];
RAC(self.avatarImageView, image) = RACObserve(self, viewModel.tweetAuthorAvatarImage);
RAC(self.userNameLabel, text) = RACObserve(self, viewModel.tweetAuthorFullName);
RAC(self.tweetTextLabel, text) = RACObserve(self, viewModel.tweetContent);
}
讓我們審視一下這段代碼涯贞。
RAC(self.viewModel, username) = [myTextfield rac_textSignal];
這里我們用RAC的庫方法從UITextField
中抽取出一個信號量枪狂,這一行代碼將view-model的readwrite屬性username
與用戶產(chǎn)生輸入時textfield的更新綁定起來。
RACSignal *usernameIsValidSignal = RACObserve(self.viewModel, usernameValid);
RAC(self.goButton, alpha) = [usernameIsValidSignal
map: ^(NSNumber *valid) {
return valid.boolValue ? @1 : @0.5;
}
];
RAC(self.goButton, enabled) = usernameIsValidSignal;
這里我們使用RACObserve
在view-model的usernameValid
屬性上創(chuàng)建了一個信號量usernameIsValidSignal
宋渔。每當這個屬性發(fā)生變化的時候州疾,該信號量會傳出一個@YES
或@NO
的值。我們將這個值與goButton
的兩個屬性綁定起來皇拣。首先严蓖,我們根據(jù)值是YES或NO,分別將alpha
設(shè)置成1或者0.5(記住我們要傳的是一個NSNumber
類型)氧急。接著我們將該值直接與enabled
屬性綁定起來颗胡,因為該屬性剛好是一個BOOL類型,所以不需要做任何轉(zhuǎn)換吩坝。
RAC(self.avatarImageView, image) = RACObserve(self.viewModel, userAvatarImage);
RAC(self.userNameLabel, text) = RACObserve(self.viewModel, userFullName);
接下來我們還是用宏RACObserve
,將imageView和Label分別綁定到view-model對應(yīng)的屬性上去毒姨。
@weakify(self);
[[[RACSignal merge:@[RACObserve(self.viewModel, tweets), RACObserve(self.viewModel, allTweetsLoaded)]]
bufferWithTime:0 onScheduler:[RACScheduler mainThreadScheduler]]
subscribeNext:^(id value) {
@strongify(self);
[self.tableView reloadData];
}];
這一段代碼可能有點復(fù)雜。我們希望當view-model的tweets
數(shù)組和allTweetsLoaded
屬性改變時更新tableview(在這個例子中钾恢,我們簡單地更新了整個tableview)手素。所以我們用RACObserve
創(chuàng)建了這兩個屬性的信號量,并合并成一個更大的信號量:當這兩個屬性任意一個發(fā)生變化的時候瘩蚪,合并后的信號量會傳出一個值(通常你會希望一個信號量傳出的值都是同一類型的泉懦,而不是像這個例子中混合的類型。這個用RAC Swift是會強迫保證的疹瘦。但這里我們并不關(guān)心實際傳出的值崩哩,我們只是用它來觸發(fā)tableview的刷新)。
這里看起來有點復(fù)雜的是接在后面的bufferWithTime:onScheduler:
方法言沐。這是為了解決UIKit中的一個問題邓嘹。我們需要追蹤這兩個屬性,tweets
和allTweetsLoaded
的變化险胰,并在其中任意一個發(fā)生變化時刷新tableview汹押。有時,這兩個屬性會在同一時間發(fā)生變化起便,這意味著棚贾,合并的信號量中的兩個單獨的信號量會同時傳出一個值窖维,reloadData
方法會在同一個run loop中調(diào)用兩次。UIKit并不允許這樣的做法妙痹。bufferWithTime:
方法將一定時間內(nèi)所有待傳遞的值存起來铸史,并在這段時間之后打包發(fā)送給訂閱者。如果傳入的參數(shù)為0怯伊,bufferWithTime:
將會保存我們合并后的信號量在一個特定run loop中傳遞的所有的值琳轿,然后將它們一并發(fā)出(原注10:NSTimer的工作方法是相同的。這也不是巧合啦哈哈耿芹,因為bufferWithTime:
就是用NSTimer
實現(xiàn)的)≌复郏現(xiàn)在不用去想scheduler,就把它想象成指定了這些值必須是在主線程傳遞⌒上担現(xiàn)在媚送,我們保證了reloadData
方法每一個run loop都只執(zhí)行一次。
注意我這里使用的strong weak dance寇甸,就是@weakify/@strongify
這些宏。當我們使用這些block的時候疗涉,這是非常重要的拿霉!當在RAC block中使用self
的時候,如果不仔細咱扣,很容易會使得self被block所持有绽淘,從而產(chǎn)生循環(huán)引用。
[[self.goButton rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext: ^(id value) {
@strongify(self);
[self.viewModel getTweetsForCurrentUsername];
}];
這是我的下一篇文章中會講到的RACCommand
使用的地方闹伪。但這里沪铭,我們只是當按鈕被點擊時,手動調(diào)用了view-model的getTweetsForCurrentUsername
方法偏瓤。
我們已經(jīng)講過了cellForRowAtIndexPath
的第一個部分杀怠,現(xiàn)在看一下loading cell的部分:
MYLoadingCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"MYLoadingCell" forIndexPath:indexPath];
[self.tableView loadMoreTweets];return cell;
這是另一個將來會用RACCommand
的地方,不過現(xiàn)在我們也是手動調(diào)用了view-model的loadMoreTweets
方法厅克。我們默認在cell重用的時候赔退,view-model內(nèi)部會有機制防止該方法重復(fù)調(diào)用。
- (void) awakeFromNib {
[super awakeFromNib]; RAC(self.avatarImageView, image) = RACObserve(self, viewModel.userAvatarImage);
RAC(self.userNameLabel, text) = RACObserve(self, viewModel.tweetAuthorFullName);
RAC(self.tweetTextLabel, text) = RACObserve(self, viewModel.tweetContent);
}
這段代碼意思很明確证舟,但我還要指出一點:我們將一個image和一些string綁定在UI的相關(guān)屬性上硕旗。但注意到,viewModel
是處于RACObserve()
宏的右邊參數(shù)位置女责。這些cell將會被重用漆枚,新的view-model會被賦給它們。如果將viewModel
放在左邊參數(shù)的位置抵知,就相當于監(jiān)聽viewModel
屬性的變化墙基,并每次都重新進行綁定昔榴;相反,將viewModel
放在右邊參數(shù)的位置碘橘,RACObserve
會為我們做好這些工作互订。因此,我們只需要做一次這些綁定的工作痘拆,剩下的工作交給Reactive Cocoa吧仰禽!在綁定cell的時候要記住這一點。實際使用中我從沒碰到過坑纺蛆,即使是在大量cell復(fù)用的時候吐葵。
譯者注:
-
胖model和瘦model ?
-
是否是說,從原始數(shù)據(jù)轉(zhuǎn)化成的model(也許item更合適)稱為數(shù)據(jù)模型桥氏,model保存了若干這樣的數(shù)據(jù)模型温峭,這種結(jié)構(gòu) ?
-
什么情況下呢? ?
-
參考Imperative and Declarative Programming一文:http://theproactiveprogrammer.com/design/imperative-and-declarative-programming/?utm_source=tuicool&utm_medium=referral ?
-
個人理解字支,即由一個信號量出發(fā)凤藏,經(jīng)過一系列整合,分離堕伪,轉(zhuǎn)化的過程揖庄,最后止于訂閱者的一條鏈 ?
-
RACMulticastConnection ?