原文鏈接: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ù)。但此外,它也負(fù)責(zé)收集、解釋、轉(zhuǎn)換這些數(shù)據(jù)。這就使得view (controller)有了更清晰專一的職責(zé):將view-model提供的數(shù)據(jù)展示出來。
更多關(guān)于view-model
view-model這個詞組的確不能表達我們的意思。一個更好的表述應(yīng)當(dā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ù)模型對象)。它也負(fù)責(zé)對上游數(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,能夠更準(zhǔn)確反映各個部分之間的關(guān)系。對于MVVM,我們采取一樣的做法,把V(View)移動到VM的右邊,得到MVMV(我確定最初不采取這種更直觀的命名是有原因的)。
下圖描述了這兩種模式在iOS中是如何融為一體的:
- 我盡量將各個方塊的大小與各部分所承擔(dān)的任務(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
,展示當(dāng)前查找的用戶的頭像和名字。 - 下方有一個
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
屬性 - 當(dāng)
usernameValid
為NO時將按鈕的alpha
值設(shè)為.5(當(dāng)usernameValid
為YES時為1.0) - 用
userFullName
里的字符串來更新UILabel的text
屬性 - 使用
userAvatarImage
的值來更新UIImageView的Image
- 使用
tweets
里的對象來設(shè)置tableview cell - 當(dāng)tableview 滑到底時,如果
allTweetsLoaded
的值為NO,需要添加一個"loading" cell
View controller可能以下列方式來操作view-model:
- 當(dāng)UITextField里的text改變時,更新我們view-model中唯一的
readwrite
屬性,username
- 當(dāng)用戶點擊"Go"按鈕時,調(diào)用view-model的
getTweetsForCurrentUsername
方法 - 當(dāng)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:如果你習(xí)慣使用一些UIImageView的category來加載網(wǎng)絡(luò)圖片的話,你可以不暴露一個UIImage,而是暴露一個URL,這確實能夠?qū)iew-model和UIKit分隔得更清楚。但就我自己而言,我更將UIIMage看作一種數(shù)據(jù),而不是用來表現(xiàn)數(shù)據(jù)的視圖。這里并沒有明顯的界線)
- 做許多費力的活兒
再一次,注意到對于view-model的變化的作出響應(yīng)的責(zé)任在view controller中。
Child View-Model
之前提到,使用tweets
里的對象來設(shè)置tableview cell。通常你希望這些對象是代表一條條推特的數(shù)據(jù)模型對象。你可能會感到疑惑:誒不是說MVVM中我們盡量不暴露數(shù)據(jù)模型的嗎?(原注3)
并不是一個view-model代表了屏幕上所有的東西。我們可以使用child view-model來代表屏幕上更小的,更模塊化的元素。當(dāng)這種元素可以被重用(比如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án)格來講,你應(yīng)當(dāng)在app delegate中為你的top view controller創(chuàng)建一個view-model。當(dāng)present一個新的view controller,或者一個新的由view-model代表的view的時候,你請求當(dāng)前的view-model來創(chuàng)建一個child view-model。
例如,我們想增加一個資料頁(profile view controller),用以當(dāng)用戶點擊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一個當(dāng)前用戶的profile view controller,但這個profile view controller需要一個view-model。我們這里的view controller并不知道創(chuàng)建這個profile view controller所需要的全部信息(當(dāng)然,它也不應(yīng)該知道),因此,view controller會讓自己的view-model來做這個工作。
View-Models 列表
在這個demo中,當(dāng)我們收到數(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)用的其它部分。什么意思呢?回想一下剛開始學(xué)習(xí)編程的時候?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)的,令人討厭的工作(。。。)至少這是我開始接觸做這些時的感想。我甚至讀過一兩本這方面的書,但當(dāng)他們開始偽造(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分離開了,這就意味著不需要偽造,偷換等等瘋狂的舉動就可以寫出很好的測試了。
連接一切
那么,當(dāng)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?當(dāng)view-model中屬性發(fā)生變化的時候拋出一個通知?Noooooooooo!
我們的view controller確實會知道view-model中的一些變化。我們可以利用UITextField
代理方法,每當(dāng)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)在讓我們看看吧!
若有一個表單,當(dāng)表單判斷為有效時,更新一個提交按鈕的狀態(tài)??紤]如何控制這些信息的流動?,F(xiàn)在的你可能是這么做的:
最終,你小心地使用這些狀態(tài)變量,在代碼里各個不相關(guān)的地方處理這個簡單的邏輯??吹竭@個信息流的許多不同入口了嗎?(這還只是一個UI元素的一套邏輯而已?。?a target="_blank" rel="nofollow">我們所使用的這種抽象并不聰明,不能為我們追蹤這些事情之間的關(guān)系,所以我們只好可憐兮兮地自己來做這些事情。
讓我們來看看聲明式的做法
這看起來有點像以前學(xué)校里CS課上使用的APPLICATION流程圖。利用聲明式編程方法,我們使用一種更高的抽象,使得我們編程的方法更接近我們腦海中設(shè)計應(yīng)用流程圖的過程。我們把更多的事情丟給計算機去做。實際的代碼現(xiàn)在更接近這張圖了。
RACSignal
RACSignal
(signal,信號量)是RAC所有內(nèi)容的基石。它是一個代表我們最終會收到的信息的對象。當(dāng)你能夠?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)在,我可以訂閱這個信號量了,每當(dāng)有網(wǎng)絡(luò)請求響應(yīng)返回的時候,我都能收到一個json值或是一個error。
幸運的是,RAC框架的創(chuàng)造者們在實際項目中也運用著該框架(我猜的),因此,他們很清楚我們的代碼中最需要的是什么。他們提供給我們一套很好的機制,來將我們以前習(xí)慣使用的異步模式轉(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
會生成一個信號量,并立刻將該屬性當(dāng)前的值傳出(如果有訂閱者的話),并且將來該屬性的值變化的話,該變化值也會由此傳出。
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)掀饋?!酷!它們會成為上文聲明式流程圖中的節(jié)點。這是多么讓人雞凍??!
什么是訂閱者(subscriber)?
簡單來說,一個訂閱者就是一段代碼,它等待信號量傳來的值,并用這些值來做一些事情(當(dāng)然,也可以用“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
的原始值類型。不過不用擔(dān)心,RAC大部分情況下都會為你自動轉(zhuǎn)換好。
更棒的是,RAC的創(chuàng)造者認(rèn)為,像這種將一個屬性的值綁定到另一個屬性上,并監(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ù)雜的操作
當(dāng)訂閱一個信號量鏈的時候,有一點很重要的事情要注意:每次有一個新的值通過信號量鏈[5]傳遞的時候,每有一個訂閱者,值就會被傳遞一次,這些值不會在任何地方被存儲起來(除了RAC內(nèi)部實現(xiàn)的時候)。當(dāng)一個信號量想傳遞出去一個新值的時候,它會遍歷其所有訂閱者,對每一個訂閱者都傳一次。(這是信號量鏈的一個簡化了的解釋,但基本思想是對的)
了解這點為什么重要呢?這意味著,在這一信號量鏈中某處產(chǎn)生的副作用,任何影響應(yīng)用的轉(zhuǎn)換,都會重復(fù)發(fā)生多次。這通常是剛接觸RAC的使用者所不希望的。(這也違背了函數(shù)式data in, data out的原則)
舉一個略顯刻意的例子:有一個按鈕點擊事件的信號量,在信號量鏈中的某處,會增加self
的counter屬性。如果有多個訂閱者訂閱了這個信號量鏈,counter屬性會比你預(yù)期的增加得更多。你必須盡量消除信號量鏈中產(chǎn)生的副作用。如果實在不能避免中間的副作用,你也可以使用一些方法來防止副作用的影響[6]。我將在另一篇文章中講解。
除了副作用,你也要當(dāng)心那些包含耗時操作或可變數(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ò)請求速度較慢。
舉例:我們有一個信號量,每當(dāng)一個按鈕被點擊的時候,會傳出一個值。我們想將這個值通過網(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
。每當(dāng)這個屬性發(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ù)雜。我們希望當(dāng)view-model的tweets
數(shù)組和allTweetsLoaded
屬性改變時更新tableview(在這個例子中,我們簡單地更新了整個tableview)。所以我們用RACObserve
創(chuàng)建了這兩個屬性的信號量,并合并成一個更大的信號量:當(dā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)的)?,F(xiàn)在不用去想scheduler,就把它想象成指定了這些值必須是在主線程傳遞?,F(xiàn)在,我們保證了reloadData
方法每一個run loop都只執(zhí)行一次。
注意我這里使用的strong weak dance,就是@weakify/@strongify
這些宏。當(dāng)我們使用這些block的時候,這是非常重要的!當(dāng)在RAC block中使用self
的時候,如果不仔細,很容易會使得self被block所持有,從而產(chǎn)生循環(huán)引用。
[[self.goButton rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext: ^(id value) {
@strongify(self);
[self.viewModel getTweetsForCurrentUsername];
}];
這是我的下一篇文章中會講到的RACCommand
使用的地方。但這里,我們只是當(dāng)按鈕被點擊時,手動調(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
方法。我們默認(rèn)在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ù)的位置,就相當(dāng)于監(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 ?