[譯]ReactiveCocoa and MVVM, an Introduction

原文鏈接: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)是否是不明智的呢?
!原因有兩點:

  1. Apple并沒有指導(dǎo)我們?nèi)绾稳ソ鉀QMassive View Controller的問題。它讓我們自己去解決,去為我們的代碼添加更有效的修改(原注1:今年的WWDC上,一些Apple的示例代碼中也出現(xiàn)了view model)。MVVM是一個很好的解決的途徑。
  2. MVVM,或者說我接下來將要展示的MVVM的編碼模式,是很符合MVC模式規(guī)范的,就好像是我們將MVC向前自然地推進了一步。

MVVM的定義

  1. Model——MVVM中的model的含義并沒有變化。你的model有可能包含一些業(yè)務(wù)邏輯,這取決于你自己[1]。我傾向于用它來作為一個保存數(shù)據(jù)模型對象的結(jié)構(gòu)[2],而將創(chuàng)建或管理model的邏輯放到一個單獨的manager類型的類中。
  2. View——view包含了UI(不管是UIView代碼,storyboard還是xibs),view的邏輯,以及用戶輸入響應(yīng)。在iOS開發(fā)中,這其中很多是UIViewController所做的事情,而不僅僅是UIView。
  3. 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",但信號量的作用更強大,不僅僅是只傳遞一次返回值而已。


A signal with a subscriber
A signal with a subscriber

上文提過,我們可以根據(jù)需要對信號量傳遞出的值進行過濾,轉(zhuǎn)換,分離,整合等。不同的訂閱者可能會以不同的方式來使用信號量傳出的這些值。


A signal with two subscribers
A signal with two subscribers

信號量是從哪兒獲得它們所傳遞的這些值的呢?

信號量是一段異步的代碼,等待某些事情發(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);
A signal created with RACObserve
A signal created with RACObserve

這僅僅是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ò)請求包含了以上所說的三個注意點:

  1. 網(wǎng)絡(luò)請求影響了你app的網(wǎng)絡(luò)層(副作用)
  2. 網(wǎng)絡(luò)請求為你的信號量鏈引入了可變數(shù)據(jù)(兩個完全相同的網(wǎng)絡(luò)請求可能會返回不同的數(shù)據(jù))
  3. 網(wǎng)絡(luò)請求速度較慢。

舉例:我們有一個信號量,每當(dāng)一個按鈕被點擊的時候,會傳出一個值。我們想將這個值通過網(wǎng)絡(luò)請求轉(zhuǎn)換為另一個結(jié)果。如果這個信號量鏈有多個訂閱者要使用最后的值,這中間就會產(chǎn)生多次的網(wǎng)絡(luò)請求。


A signal with side effects happening twice
A signal with side effects happening twice

顯然,網(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中的一個問題。我們需要追蹤這兩個屬性,tweetsallTweetsLoaded的變化,并在其中任意一個發(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ù)用的時候。

譯者注:


  1. 胖model和瘦model ?

  2. 是否是說,從原始數(shù)據(jù)轉(zhuǎn)化成的model(也許item更合適)稱為數(shù)據(jù)模型,model保存了若干這樣的數(shù)據(jù)模型,這種結(jié)構(gòu) ?

  3. 什么情況下呢? ?

  4. 參考Imperative and Declarative Programming一文:http://theproactiveprogrammer.com/design/imperative-and-declarative-programming/?utm_source=tuicool&utm_medium=referral ?

  5. 個人理解,即由一個信號量出發(fā),經(jīng)過一系列整合,分離,轉(zhuǎn)化的過程,最后止于訂閱者的一條鏈 ?

  6. RACMulticastConnection ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容