ReactiveCocoa系列教程1-基本概念

此篇文章主要介紹了MVC和MVVM的區(qū)別和關(guān)系厕氨;同時(shí)闡述了有關(guān)函數(shù)式的概念捆交;解釋了ReactiveCocoa的工作原理,文章內(nèi)容過(guò)于概念化腐巢,如果看不懂可以先收藏保存品追,看完之后的入門(mén)教程之后再回頭閱讀此文章大有助益
文章內(nèi)容篇幅過(guò)多,所以沒(méi)有將文章內(nèi)實(shí)例代碼一一使用swift編寫(xiě)冯丙;以后的教程中都將使用Swift語(yǔ)言進(jìn)行編寫(xiě)
文章轉(zhuǎn)載自 玉令天下的博客 肉瓦;在此感謝

MVC

任何一個(gè)正經(jīng)開(kāi)發(fā)過(guò)一陣子軟件的人都熟悉MVC. 它意思是Model View Controller, 是一個(gè)在復(fù)雜應(yīng)用設(shè)計(jì)中組織代碼的公認(rèn)模式. 它也被證實(shí)在 iOS 開(kāi)發(fā)中有著第二種含義: Massive View Controller(重量級(jí)視圖控制器). 它讓許多程序員絞盡腦汁如何去使代碼被解耦和組織地讓人滿(mǎn)意. 總的來(lái)說(shuō), iOS 開(kāi)發(fā)者已經(jīng)得出結(jié)論: 他們需要給視圖控制器瘦身, 并進(jìn)一步分離事物;但該怎么做呢?

MVVM

于是MVVM流行起來(lái), 它代表Model View View-Model, 它在這幫助我們創(chuàng)建更易處理, 更佳設(shè)計(jì)的代碼.
有時(shí)候違背蘋(píng)果建議的編碼方式并不是個(gè)好做法. 我不是說(shuō)不贊成這樣子, 我指的是可能會(huì)弊大于利. 比如我不建議你去實(shí)現(xiàn)個(gè)自己的 view controller 基類(lèi)并試著自己處理視圖生命周期.
帶著這種情緒, 我想提個(gè)問(wèn)題: 使用除蘋(píng)果推薦的 MVC 之外的應(yīng)用設(shè)計(jì)模式是愚蠢的么?
不. 有兩個(gè)原因.

  1. 蘋(píng)果沒(méi)有為解決重量級(jí)試圖控制器問(wèn)題提供真正的指導(dǎo). 他們留給我們來(lái)解決如何向代碼添加更多清晰的思路. 用 MVVM 來(lái)實(shí)現(xiàn)這個(gè)目的想必是極好噠. (在今年 WWDC 的一些視頻中, 蘋(píng)果工程師在屏幕上的示例代碼的確少許出現(xiàn)了 view-model, 不知道是否因?yàn)橛兴懦蔀榱耸纠a)
  2. MVVM, 至少是我將要在這里展示的 MVVM 的風(fēng)格, 都跟 MVC 十分兼容. 仿佛我們將 MVC 進(jìn)行到下一個(gè)邏輯步驟.
    我不會(huì)提及 MVC/MVVM 的歷史, 因?yàn)槠渌胤揭呀?jīng)有所介紹, 并且我也不精通. 我將會(huì)關(guān)注如何用它進(jìn)行 iOS/Mac 開(kāi)發(fā).

定義 MVVM

  1. Model - model 在 MVVM 中沒(méi)有真正的變化. 取決于你的偏好, 你的 model 可能會(huì)或可能不會(huì)封裝一些額外的業(yè)務(wù)邏輯工作. 我更傾向于把它當(dāng)做一個(gè)容納表現(xiàn)數(shù)據(jù)-模型對(duì)象信息的結(jié)構(gòu)體, 并在一個(gè)單獨(dú)的管理類(lèi)中維護(hù)的創(chuàng)建/管理模型的統(tǒng)一邏輯.
  2. View - view 包含實(shí)際 UI 本身(不論是 UIView 代碼, storyboard 和 xib), 任何視圖特定的邏輯, 和對(duì)用戶(hù)輸入的反饋. 在 iOS 中這不僅需要 UIView 代碼和那些文件, 還包括很多需由 UIViewController 處理的工作.
  3. View-Model - 這個(gè)術(shù)語(yǔ)本身會(huì)帶來(lái)困惑, 因?yàn)樗齑盍藘蓚€(gè)我們已知的術(shù)語(yǔ), 但卻是完全不同的東東. 它不是傳統(tǒng)數(shù)據(jù)-模型結(jié)構(gòu)中模型的意思(又來(lái)了, 只是我喜歡這個(gè)例子). 它的職責(zé)之一就是作為一個(gè)表現(xiàn)視圖顯示自身所需數(shù)據(jù)的靜態(tài)模型;但它也有收集, 解釋和轉(zhuǎn)換那些數(shù)據(jù)的責(zé)任. 這留給了 view (controller) 一個(gè)更加清晰明確的任務(wù): 呈現(xiàn)由 view-model 提供的數(shù)據(jù).

關(guān)于 view-model 的更多內(nèi)容

view-model 一詞的確不能充分表達(dá)我們的意圖. 一個(gè)更好的術(shù)語(yǔ)可能是 “View Coordinator”(感謝Dave Lee提的這個(gè) “View Coordinator” 術(shù)語(yǔ), 真是個(gè)好點(diǎn)子). 你可以認(rèn)為它就像是電視新聞主播背后的研究人員和作家團(tuán)隊(duì). 它從必要的資源(數(shù)據(jù)庫(kù), 網(wǎng)絡(luò)服務(wù)調(diào)用, 等)中獲取原始數(shù)據(jù), 運(yùn)用邏輯, 并處理成 view (controller) 的展示數(shù)據(jù). 它(通常通過(guò)屬性)暴露給視圖控制器需要知道的僅關(guān)于顯示視圖工作的信息(理想地你不會(huì)暴漏你的 data-model 對(duì)象). 它還負(fù)責(zé)對(duì)上游數(shù)據(jù)的修改(比如更新模型/數(shù)據(jù)庫(kù), API POST 調(diào)用).

MVC 世界中的 MVVM

我認(rèn)為 MVVM 這個(gè)首字母縮寫(xiě)如同 view-model 術(shù)語(yǔ)一樣, 對(duì)如何使用它們進(jìn)行 iOS 開(kāi)發(fā)體現(xiàn)得有點(diǎn)不太準(zhǔn)確. 讓我們?cè)贆z查下這個(gè)首字母縮寫(xiě), 了解下它是怎么與 MVC 融為一體的.
為了圖解表示, 我們顛倒了 MVC 中的 V 和 C, 于是首字母縮寫(xiě)更能準(zhǔn)確地反映出組件間的關(guān)系方位, 給我們帶來(lái) MCV. 我也會(huì)對(duì) MVVM 這么干, 將 V(iew) 移到 VM 的右邊最終成為了 MVMV. (我相信這些首字母縮寫(xiě)起初不排成這樣更合理的順序是有原因的. )
這是這兩種模式如何在 iOS 中組裝在一起的簡(jiǎn)單映射:

MCVMVMV
  • 我試圖遵循區(qū)塊尺寸(非常)大致對(duì)應(yīng)它們負(fù)責(zé)的工作量.
  • 注意到視圖控制器有多大?
  • 你可以看到我們巨大的視圖控制器和 view-model 之間有大塊工作上的重合.
  • 你也可以看看視圖控制器在 MVVM 中的足跡有多大一部分是跟視圖重合的.

你大可安心獲知我們并沒(méi)有真的去除視圖控制器的概念或拋棄 “controller” 術(shù)語(yǔ)來(lái)匹配 MVVM. (唷. )我們正要將重合的那塊工作剝離到 view-model 中, 并讓視圖控制器的生活更加簡(jiǎn)單.

我們實(shí)際上最終以 MVMCV 告終. Model View-Model Controller View. 我確信我無(wú)拘無(wú)束的應(yīng)用設(shè)計(jì)模式駭客行為會(huì)讓人大吃一驚.

MCVMVMV

我們的結(jié)果:

MVMCV

現(xiàn)在視圖控制器僅關(guān)注于用 view-model 的數(shù)據(jù)配置和管理各種各樣的視圖, 并在先關(guān)用戶(hù)輸入時(shí)讓 view-model 獲知并需要向上游修改數(shù)據(jù). 視圖控制器不需要了解關(guān)于網(wǎng)絡(luò)服務(wù)調(diào)用, Core Data, 模型對(duì)象等. (事實(shí)上有時(shí)通過(guò) view-model 頭文件而不是復(fù)制一大堆屬性來(lái)暴漏 model 是很務(wù)實(shí)的, 后面還會(huì)有)
view-model 會(huì)在視圖控制器上以一個(gè)屬性的方式存在. 視圖控制器知道 view-model 和它的公有屬性, 但是 view-model 對(duì)視圖控制器一無(wú)所知. 你早就該對(duì)這個(gè)設(shè)計(jì)感覺(jué)好多了因?yàn)槲覀兊年P(guān)注點(diǎn)在這兒進(jìn)行更好地分離.

幫助你理解我們?nèi)绾伟呀M件組裝在一起還有組件對(duì)應(yīng)職責(zé)的另一種方式, 就是著眼于我們新的應(yīng)用構(gòu)建模塊層級(jí)圖.

mvvm-layers

View-Model 和 View Controller, 在一起,但獨(dú)立

我們來(lái)看個(gè)簡(jiǎn)單的 view-model 頭文件來(lái)對(duì)我們新構(gòu)件的長(zhǎng)相有個(gè)更好地概念. 為了情節(jié)簡(jiǎn)單, 我們構(gòu)建按了一個(gè)偽造的推特客戶(hù)端來(lái)查看任何推特用戶(hù)的最新回復(fù), 通過(guò)輸入他們的姓名并點(diǎn)擊 “Go”. 我們的樣例界面將會(huì)是這樣:

  • 有一個(gè)讓用戶(hù)輸入他們姓名的 UITextField , 和一個(gè)寫(xiě)著 “Go” 的 UIButton
  • 有顯示被查看的當(dāng)前用戶(hù)頭像和姓名的 UIImageView 和 UILabel 各一個(gè)
  • 下面放著一個(gè)顯示最新回復(fù)推文的 UITableView
  • 允許無(wú)限滾動(dòng)
tweeboatplus

View-Model 實(shí)例

我們的 view-model 頭文件應(yīng)該長(zhǎng)這樣:

@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;

相當(dāng)直截了當(dāng)?shù)奶畛? 注意到這些壯麗的 readonly 屬性了么?這個(gè) view-model 暴漏了視圖控制器所必需的最小量信息, 視圖控制器實(shí)際上并不在乎 view-model 是如何獲得這些信息的. 現(xiàn)在我們兩者都不在乎. 僅僅假定你習(xí)慣于標(biāo)準(zhǔn)的網(wǎng)絡(luò)服務(wù)請(qǐng)求, 校驗(yàn), 數(shù)據(jù)操作和存儲(chǔ).

view-model 不做的事兒

  • 對(duì)視圖控制器以任何形式直接起作用或直接通告其變化

View Controller(視圖控制器)

視圖控制器從 view-model 獲取的數(shù)據(jù)將用來(lái):

  • 當(dāng) usernameValid 的值發(fā)生變化時(shí)觸發(fā) “Go” 按鈕的 enabled 屬性
  • 當(dāng) usernameValid 等于 NO 時(shí)調(diào)整按鈕的 alpha 值為0. 5(等于 YES 時(shí)設(shè)為1. 0)
  • 更新 UILable 的 text 屬性為字符串 userFullName 的值
  • 更新 UIImageView 的 image 屬性為 userAvatarImage 的值
  • 用 tweets 數(shù)組中的對(duì)象設(shè)置表格視圖中的 cell (后面會(huì)提到)
  • 當(dāng)滑到表格視圖底部時(shí)如果 allTweetsLoaded 為 NO, 提供一個(gè) 顯示 “l(fā)oading” 的 cell

視圖控制器將對(duì) view-model 起如下作用:

  • 每當(dāng) UITextField 中的文本發(fā)生變化, 更新 view-model 上僅有的 readwrite 屬性 username
  • 當(dāng) “Go” 按鈕被按下時(shí)調(diào)用 view-model 上的 getTweetsForCurrentUsername 方法
  • 當(dāng)?shù)竭_(dá)表格中的 “l(fā)oading” cell 時(shí)調(diào)用 view-model 上的 loadMoreTweets 方法

視圖控制器不做的事兒:

  • 發(fā)起網(wǎng)絡(luò)服務(wù)調(diào)用
  • 管理 tweets 數(shù)組
  • 判定 username 內(nèi)容是否有效
  • 將用戶(hù)的姓和名格式化為全名
  • 下載用戶(hù)頭像并轉(zhuǎn)成 UIImage(如果你習(xí)慣在 UIImageView 上使用類(lèi)別從網(wǎng)絡(luò)加載圖片, 你可以暴漏 URL 而不是圖片. 這樣就給 view-model 與 UIKit 之間一個(gè)更清晰的劃分, 但我視 UIImage 為數(shù)據(jù)而非數(shù)據(jù)的確切顯示. 這些東西不是固定死的. )
    流汗

請(qǐng)?jiān)俅巫⒁庖晥D控制器總的責(zé)任是處理 view-model 中的變化.

子 View-Model

我提到過(guò)使用 view-model 上的 tweets 數(shù)組中的對(duì)象配置表格視圖的 cell.通常你會(huì)期待展現(xiàn) tweets 的是數(shù)據(jù)-模型對(duì)象. 你可能已經(jīng)對(duì)其感到奇怪, 因?yàn)槲覀冊(cè)噲D通過(guò) MVVM 模式不暴漏數(shù)據(jù)-模型對(duì)象. (前面提到過(guò)的)

view-model 不必在屏幕上顯示所有東西. 你可用子 view-model 來(lái)代表屏幕上更小, 更潛在被封裝的部分. 如果一個(gè)視圖上的一小塊兒(比如表格的 cell)在 app 中可以被重用以及(或)表現(xiàn)多個(gè)數(shù)據(jù)-模型對(duì)象, 子 view-model 會(huì)格外有利.

你不總是需要子 view-model. 比如, 我可能用表格 header 視圖來(lái)渲染我們“tweetboat plus”應(yīng)用的頂部. 它不是個(gè)可重用的組件, 所以我可能僅是將我們已經(jīng)給視圖控制器用過(guò)的相同的 view-model 傳給那個(gè)自定義的 header 視圖. 它會(huì)用到 view-model 中它需要的信息, 而無(wú)視余下的部分. 這對(duì)于保持子視圖同步是極好的方式, 因?yàn)樗鼈兛梢杂行У嘏c信息中相同確切的上下文作用, 并觀察確切相同屬性的更新.
在我們的例子中, tweets 數(shù)組將會(huì)被下面這樣的子 view-model 充滿(mǎn):

//MyTweetCellViewModel.h
@interface MYTweetCellViewModel: NSObject
 
@property (nonatomic, strong, readonly) NSString *tweetAuthorFullName;
@property (nonatomic, strong, readonly) UIImage *tweetAuthorAvatarImage;
@property (nonatomic, strong, readonly) NSString *tweetContent;

你可能認(rèn)為這也太像普通”推特”里的數(shù)據(jù)-模型對(duì)象了吧. 為啥要干將其轉(zhuǎn)化成 view-model 的工作?即使類(lèi)似, view-model 讓我們限制信息只暴露給我們需要的地方, 提供額外數(shù)據(jù)轉(zhuǎn)換的屬性, 或?yàn)樘囟ǖ囊晥D計(jì)算數(shù)據(jù). (此外, 當(dāng)可以不暴露可變數(shù)據(jù)-模型對(duì)象時(shí)也是極好的, 因?yàn)槲覀兿M?view-model 自己承擔(dān)起更新它們的任務(wù), 而不是靠視圖或視圖控制器. )

View-Model 從哪來(lái)?

那么 view-model 是何時(shí)何處被創(chuàng)建的呢?視圖控制器創(chuàng)建它們自己的 view-model 么?

View-Model 產(chǎn)生 View-Model

嚴(yán)格來(lái)說(shuō), 你應(yīng)該為 app delegate 中的頂級(jí)視圖控制器創(chuàng)建一個(gè) view-model. 當(dāng)展示一個(gè)新的視圖控制器時(shí), 或很小的視圖被 view-model 表現(xiàn)時(shí), 你應(yīng)要求當(dāng)前的 view-model 為你創(chuàng)建一個(gè)子 view-model.

child-view-models

加入我們想要在用戶(hù)輕拍應(yīng)用頂部的頭像時(shí)添加一個(gè)資料視圖控制器. 我們可以為一級(jí) view-model 添加類(lèi)似如下方法:

- (MYTwitterUserProfileViewModel *) viewModelForCurrentUser;

然后在我們的一級(jí)視圖控制器中這么用它:

//MYMainViewController.m 
- (IBAction) didTapPrimaryUserAvatar
{
    MYTwitterUserProfileViewModel *userProfileViewModel = [self.viewModel viewModelForCurrentUser];
    
    MYTwitterUserProfileViewController *profileViewController = 
        [[MYTwitterUserProfileViewController alloc] initWithViewModel: userProfileViewModel];
    [self.navigationController pushViewController: profileViewController animated:YES];
}

在這個(gè)例子中我將會(huì)展現(xiàn)當(dāng)前用戶(hù)的資料視圖控制器, 但是我的資料視圖控制器需要一個(gè) view-model. 我這的主視圖控制器不知道(也不該知道)用于創(chuàng)建關(guān)聯(lián)相關(guān)用戶(hù) view-model 的全部必要數(shù)據(jù), 所以它請(qǐng)求它自己的 view-model 來(lái)干這種創(chuàng)建新 view-model 的苦差事.

View-Model 列表

至于我們的推特 cell, 當(dāng)數(shù)據(jù)驅(qū)動(dòng)屏幕(在這個(gè)例子中或許是通過(guò)網(wǎng)絡(luò)服務(wù)調(diào)用)聚到一起時(shí), 我將會(huì)代表性地提前為對(duì)應(yīng)的 cell 創(chuàng)建所有的 view-model. 所以在我們這個(gè)方案中, tweets 將會(huì)是一個(gè) MYTweetCellViewModel 對(duì)象數(shù)組. 在我的表格視圖中的 cellForRowAtIndexPath 方法中, 我將會(huì)在正確的索引上簡(jiǎn)單地抓取 view-model, 并把它賦值給我的 cell 上的 view-model 屬性.

Functional Core, Imperative Shell

view-model 這種通往應(yīng)用設(shè)計(jì)的方法是一塊應(yīng)用設(shè)計(jì)之路上的墊腳石, 這種被稱(chēng)作“Functional Core, Imperative Shell”的應(yīng)用設(shè)計(jì)由Gary Bernhardt創(chuàng)造. (我最近十分有幸去聽(tīng)Andy Matuschak關(guān)于這方面的演講, 他為”胖的數(shù)值層, 瘦的對(duì)象層”提出充分理由. 雖然觀點(diǎn)相似, 但關(guān)注于我們?cè)鯓右瞥龑?duì)象和它們狀態(tài)的邊界影響性質(zhì), 并用 Swift 中的新數(shù)據(jù)結(jié)構(gòu)構(gòu)建更加函數(shù)式, 可測(cè)試的數(shù)值層. )

Functional Core

view-model 就是 “functional core”, 盡管實(shí)際上在 iOS/Objective-C 中達(dá)到純函數(shù)水平是很棘手的(Swift 提供了一些附加的函數(shù)性, 這會(huì)讓我們更接近). 大意是讓我們的 view-model 盡可能少的對(duì)剩余的”應(yīng)用世界”的依賴(lài)和影響. 那意味著什么?想起你第一次學(xué)編程時(shí)可能學(xué)到的簡(jiǎn)單函數(shù)吧. 它們可能接受一兩個(gè)參數(shù)并輸出一個(gè)結(jié)果. 數(shù)據(jù)輸入, 數(shù)據(jù)輸出.這個(gè)函數(shù)可能是做一些數(shù)學(xué)運(yùn)算或是將姓和名結(jié)合到一起. 無(wú)論應(yīng)用的其他地方發(fā)生啥, 這個(gè)函數(shù)總是對(duì)相同的輸入產(chǎn)生相同的輸出. 這就是函數(shù)式方面.

這就是我們?yōu)?view-model 謀求的東西. 他們富有邏輯和轉(zhuǎn)換數(shù)據(jù)并將結(jié)果存到屬性的功能. 理想上相同的輸入(比如網(wǎng)絡(luò)服務(wù)響應(yīng))將會(huì)導(dǎo)出相同的輸出(屬性的值). 這意味著盡可能多地消除由”應(yīng)用世界”剩余部分帶來(lái)的可能影響輸出的因素, 比如使用一堆狀態(tài). 一個(gè)好的第一步就是不要再 view-model 頭文件中引入 UIKit.h.(這是個(gè)重大原則, 但也有些灰色區(qū)域. 比如, 你可能認(rèn)為 UIImage 是數(shù)據(jù)而不是展示信息. PS: 我愛(ài)這么干. 既然這樣的話(huà)就得引入 UIKit. h 以便使用 UIImage 類(lèi))UIKit 其性質(zhì)就是將要影響許多應(yīng)用世界. 它包含很多”副作用”, 憑借改變一個(gè)值或調(diào)用一個(gè)函數(shù)將觸發(fā)很多間接(甚至未知)的改變.

更新: 剛剛看了 Andy 在函數(shù)式 Swift 會(huì)議上給出的另一個(gè)超贊的演講, 于是又想到了一些. 要清楚你的 view-model 仍然只是一個(gè)對(duì)象, 而不用維護(hù)一些狀態(tài)(否則它將不會(huì)是你視圖中非常好用的模型了. )但你仍該努力將盡可能多的邏輯移到無(wú)狀態(tài)的函數(shù)”值”中. 再重復(fù)一次, Swift在這方面比 Objective-C 更加可行.

Imperative (Declarative?) Shell

命令式外殼 (Imperative Shell) 是我們需要做所有的狀態(tài)轉(zhuǎn)換, 應(yīng)用世界改變的苦差事的地方, 為的是將 view-model 數(shù)據(jù)轉(zhuǎn)成給用戶(hù)在屏幕上看到的東西. 這是我們的視圖(控制器), 實(shí)際上我們?cè)谶@分離 UIKit 的工作. 我仍將特別注意盡可能消除狀態(tài)并用 ReactiveCocoa 這種陳述性質(zhì)的東西做這方面工作, 而 iOS 和 UIKit 在設(shè)計(jì)上是命令式的. (表格的 data source 就是個(gè)很好的例子, 因?yàn)樗奈心J綇?qiáng)制將狀態(tài)應(yīng)用到委托中, 為了當(dāng)請(qǐng)求發(fā)生時(shí)能夠?yàn)楸砀褚晥D提供信息. 實(shí)際上委托模式通常強(qiáng)制一大堆狀態(tài)的使用)

可測(cè)試的核心

iOS 的單元測(cè)試是個(gè)臟, 苦, 亂的活兒. 至少我去做的時(shí)候得出的是這么個(gè)結(jié)論. 就這方面我還出讀過(guò)一兩本書(shū), 但當(dāng)開(kāi)始做視圖控制器的 mocking 和 swizzling 使其一些邏輯可測(cè)試時(shí), 我目光呆滯. 我最終把單元測(cè)試歸入模型和任何同類(lèi)別模型管理類(lèi)中. (譯者注: mock 是測(cè)試常用的手段, 而 method swizzling 是基于 Objective-C Runtime 交換方法實(shí)現(xiàn)的黑魔法)

這個(gè)函數(shù)式核心一樣的 view-model 的最大優(yōu)點(diǎn), 除了 bug 數(shù)量隨著狀態(tài)數(shù)遞減之外, 就是變得非常能夠進(jìn)行單元測(cè)試. 如果你有那種每次輸入相同而產(chǎn)生的輸出也相同的方法, 那就非常適合單元測(cè)試的世界. 我們現(xiàn)在將我們的數(shù)據(jù)用獲取/邏輯/轉(zhuǎn)換提取出, 避免了視圖控制器的復(fù)雜性. 那意味著構(gòu)建棒棒噠測(cè)試時(shí)不需要用瘋狂的 mock 對(duì)象, method swizzling, 或其他瘋癲的變通方法(希望能有).

連接一切

***那么當(dāng) view-model 的共有屬性發(fā)生變化時(shí)我們?nèi)绾胃挛覀兊囊晥D呢? ***

絕大部分時(shí)間我們用對(duì)應(yīng)的 view-model 來(lái)初始化視圖控制器, 有點(diǎn)類(lèi)似我們剛剛在上文見(jiàn)到的:

MYTwitterUserProfileViewController *profileViewController =
    [[MYTwitterUserProfileViewController alloc] initWithViewModel:  userProfileViewModel];

有時(shí)你無(wú)法在初始化時(shí)將 view-model 傳入, 比如在 storyboard segue 或 cell dequeuing 的情況下. 這時(shí)你應(yīng)該在討論中的視圖(控制器)中暴露一個(gè)公有可寫(xiě)的 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];

有時(shí)我們可以在鉤子程序調(diào)用前傳入 view-model, 比如 initviewDidLoad, 我們可以從view-model 的屬性初始化所有 UI 元素的狀態(tài).

//dontDoThis1.m 
- (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
}

好棒!我們已經(jīng)配置好了初始值. 當(dāng) view-model 上的數(shù)據(jù)改變時(shí)怎么辦? 當(dāng)”go” 按鈕在什么時(shí)候可用了怎么辦?當(dāng)用戶(hù)標(biāo)簽和頭像在什么時(shí)候從網(wǎng)絡(luò)上下載并填充了怎么辦?

我們可以將視圖控制器暴露給 view-model, 以便于當(dāng)相關(guān)數(shù)據(jù)變化或類(lèi)似事件發(fā)送時(shí)它可以調(diào)用一個(gè) “updateUI” 方法. (別這么干. )在 view-model 上將視圖控制器作為一個(gè)委托?當(dāng) view-model 內(nèi)容有變化時(shí)發(fā)個(gè)通知?(不不不不. )

我們的視圖控制器會(huì)感知一些變化的發(fā)生. 我們可以使用從 UITextfield 得來(lái)的委托方法在每當(dāng)有字符變化時(shí)通過(guò)檢查 view-model 來(lái)更新按鈕的狀態(tài).

//dontDoThisEither.m
- (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;
}

這種方法解決的場(chǎng)景是在只有再文本框發(fā)生變化時(shí)才會(huì)影響 view-model 中的 isUsernameValid 值. 假使還有其他變量/動(dòng)作改變 isUsernameValid 的狀態(tài)將會(huì)怎么樣?對(duì)于 view-model 中的網(wǎng)絡(luò)調(diào)用會(huì)怎么樣?或許我們?cè)摓?view-model 上的方法加一個(gè)完成后回調(diào)處理, 這樣我們此時(shí)就可以更新 UI 的一切東西了?使用珍貴而笨重的 KVO 方法怎么樣?

我們或許最終使用多種多樣我們熟悉的機(jī)制將 view-model 和視圖控制器所有的接觸點(diǎn)都連起來(lái), 但你已經(jīng)知道了標(biāo)題上不是這么寫(xiě)的. 這樣在代碼中創(chuàng)建了大量的入口點(diǎn), 僅僅為了簡(jiǎn)單的更新 UI 就要在代碼中完全重新創(chuàng)建應(yīng)用狀態(tài)上下文.

我們或許最終使用多種多樣我們熟悉的機(jī)制將 view-model 和視圖控制器所有的接觸點(diǎn)都連起來(lái), 但你已經(jīng)知道了標(biāo)題上不是這么寫(xiě)的. 這樣在代碼中創(chuàng)建了大量的入口點(diǎn), 僅僅為了簡(jiǎn)單的更新 UI 就要在代碼中完全重新創(chuàng)建應(yīng)用狀態(tài)上下文.

進(jìn)入 ReactiveCocoa

ReactiveCocoa(RAC) 是來(lái)拯救我們的, 并恰好返回給我們一點(diǎn)理智. 讓我們看看如何做到.
思考在一個(gè)新的用戶(hù)頁(yè)面上控制信息的流動(dòng), 當(dāng)表單合法時(shí)更新提交按鈕的狀態(tài). 你現(xiàn)在可能會(huì)照下面這么做:

new-user-form-imperative

你最后通過(guò)使用狀態(tài), 小心翼翼地代碼中許多不同且零碎無(wú)關(guān)的內(nèi)容穿到簡(jiǎn)單的邏輯上. 看看你信息流中所有不同的入口點(diǎn)?(這還只是一個(gè) UI 元素中的一條邏輯線(xiàn). )我們程序中現(xiàn)在用的抽象概念還不夠厲害, 不能為我們追蹤所有事物的關(guān)系, 所以我們停止自己去干這蛋疼事兒.

讓我們看看陳述版本:

new-user-form-declarative

這看起來(lái)可能像是為我們應(yīng)用流程文檔中的一張老舊的計(jì)算機(jī)科學(xué)圖解. 通過(guò)陳述式的編程, 我們使用了更高層次的抽象, 來(lái)讓我們實(shí)際編程更靠近我們?cè)谀X海中設(shè)計(jì)流程的方式. 我們讓電腦為我們做更多工作. 實(shí)際的代碼更加像這幅圖了.

RACSignal

RACSignal (信號(hào))就 RAC 來(lái)說(shuō)是構(gòu)造單元. 它代表我們最終將要收到的信息. 當(dāng)你能將未來(lái)某時(shí)刻收到的消息具體表示出來(lái)時(shí), 你可以開(kāi)始預(yù)先(陳述性)運(yùn)用邏輯并構(gòu)建你的信息流,而不是必須等到事件發(fā)生(命令式).

信號(hào)會(huì)為了控制通過(guò)應(yīng)用的信息流而獲得所有這些異步方法(委托, 回調(diào) block, 通知, KVO, target/action 事件觀察, 等)并將它們統(tǒng)一到一個(gè)接口下.這只是直觀理解. 不僅是這些, 因?yàn)樾畔?huì)流過(guò)你的應(yīng)用, 它還提供給你輕松轉(zhuǎn)換/分解/合并/過(guò)濾信息的能力.

replace-async-tools

那么什么是信號(hào)呢?這是一個(gè)信號(hào):

信號(hào)是一個(gè)發(fā)送一連串值的物體. 但是我們這兒的信號(hào)啥也不干, 因?yàn)樗€沒(méi)有訂閱者. 如果有訂閱者監(jiān)聽(tīng)時(shí)(已訂閱)信號(hào)才會(huì)發(fā)信息. 它將會(huì)向那個(gè)訂閱者發(fā)送0或多個(gè)載有數(shù)值的”next”事件, 后面跟著一個(gè)”complete”事件或一個(gè)”error”事件. (信號(hào)類(lèi)似于其他語(yǔ)言/工具包中的 “promise”, 但更強(qiáng)大, 因?yàn)樗粌H限于向它的訂閱者一次只傳遞一個(gè)返回值. )

signal-with-subscribe

正如我之前提到的, 如果覺(jué)得需要的話(huà)你可以過(guò)濾, 轉(zhuǎn)換, 分解和合并那些值. 不同的訂閱者可能需要使用信號(hào)通過(guò)不同方式發(fā)送的值.

signal-map

信號(hào)發(fā)送的值是從哪獲得的?

信號(hào)是一些等待某事發(fā)生的異步代碼, 然后把結(jié)果值發(fā)送給它們的訂閱者. 你可以用 RACSignal 的類(lèi)方法 createSignal: 手動(dòng)創(chuàng)建信號(hào):

//networkSignal.m
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];
        }];

我在這用一個(gè)具有成功和失敗 block (偽造)的網(wǎng)絡(luò)操作創(chuàng)建了一個(gè)信號(hào). (如果我想讓信號(hào)在被訂閱時(shí)才讓網(wǎng)絡(luò)請(qǐng)求發(fā)生, 還可以用 RACSignal 的類(lèi)方法 defer. )我在成功的 block 里使用提供的 subscriber 對(duì)象調(diào)用 sendNext: 和 sendCompleted: 方法, 或在失敗的 block 中調(diào)用 sendError:. 現(xiàn)在我可以訂閱這個(gè)信號(hào)并將在響應(yīng)返回時(shí)接收到 json 值或是 error.

幸運(yùn)的是, RAC 的創(chuàng)造者實(shí)際上使用它們自己的庫(kù)來(lái)創(chuàng)建真的事物(捉摸一下), 所以對(duì)于我們?cè)谌粘P枰裁? 他們有很強(qiáng)烈的想法. 他們?yōu)槲覀兲峁┝撕芏鄼C(jī)制, 來(lái)從我們通常使用的現(xiàn)存的異步模式中拉取信號(hào). 別忘了如果你有一個(gè)沒(méi)有被某個(gè)內(nèi)建信號(hào)覆蓋到的異步任務(wù), 你可以很容易地用 createSignal: 或類(lèi)似方法來(lái)創(chuàng)建信號(hào).

一個(gè)被提供的機(jī)制就是 RACObserve() 宏. (如果你不喜歡宏, 你可以簡(jiǎn)單地看看罩子下面并用稍微多些冗雜的描述. 這也非常好. 在我們得到 Swift 版本的替代之前, 這也有在 Swift 中使用 RAC 的解決方案. )這個(gè)宏是 RAC 中對(duì) KVO 中那些悲慘的 API 的替代. 你只需要傳入對(duì)象和你想觀察的那個(gè)對(duì)象某屬性的 keypath. 給出這些參數(shù)后, RACObserve 會(huì)創(chuàng)建一個(gè)信號(hào), 一旦它有了訂閱者, 它就立刻發(fā)送那個(gè)屬性的當(dāng)前值, 并在發(fā)送那個(gè)屬性在這之后的任何變化.

RACSignal *usernameValidSignal = RACObserve(self.viewModel,  usernameIsValid);
signal-racobserve

這僅是提供用于創(chuàng)建信號(hào)的一個(gè)工具. 這里有幾個(gè)立即可用的方式, 來(lái)從內(nèi)置控制流機(jī)制中拉取信號(hào):

//signals.m
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)建自己的信號(hào), 包括替代那些沒(méi)有內(nèi)建支持的其他委托. 我們現(xiàn)在能夠從所有這些不連貫的異步/控制流工具中拉取出信號(hào)并將他們合并, 試想想這該多酷!這些會(huì)成為我們之前看到的陳述性圖表中的節(jié)點(diǎn). 真是興奮.

什么是訂閱者?

簡(jiǎn)言之, 訂閱者就是一段代碼, 它等待信號(hào)給它發(fā)送一些值, 然后訂閱者就能處理這些值了. (它也可以作用于 “complete” 和 “error” 事件. )

這有一個(gè)簡(jiǎn)單的訂閱者, 是通過(guò)向信號(hào)的實(shí)例方法 subscribeNext 傳入一個(gè) block 來(lái)創(chuàng)建的. 我們?cè)谶@通過(guò) RACObserve() 宏創(chuàng)建信號(hào)來(lái)觀察一個(gè)對(duì)象上屬性的當(dāng)前值, 并把它賦值給一個(gè)內(nèi)部屬性.

- (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 只處理對(duì)象, 而不處理像 BOOL 這樣的原始值. 不過(guò)不用擔(dān)心, RAC 通常會(huì)幫你這些轉(zhuǎn)換.

幸運(yùn)的是 RAC 的創(chuàng)造者也意識(shí)到這種綁定行為的普遍必要性, 所以他們提供了另一個(gè)宏 RAC(). 與 RACObserve() 相同, 你提供想要與即將到來(lái)的值綁定的對(duì)象和參數(shù), 在其內(nèi)部它所做的是創(chuàng)建一個(gè)訂閱者并更新其屬性的值. 我們的例子現(xiàn)在看起來(lái)像這樣:

- (void) viewDidLoad {
    //. . . 
    RAC(self,  usernameIsValid) = RACObserve(self.viewModel,  isUsernameValid);
}

考慮下我們的目標(biāo), 這么干有點(diǎn)傻啊. 我們不需要將信號(hào)發(fā)送的值存到屬性中(這會(huì)創(chuàng)建狀態(tài)), 我們真正要做的是用從那個(gè)值獲取到信息來(lái)更新 UI.

轉(zhuǎn)換數(shù)據(jù)流

現(xiàn)在我們進(jìn)入 RAC 為我們提供的用于轉(zhuǎn)換數(shù)值流的方法. 我們將會(huì)利用 RACSignal 的實(shí)例方法 map.

//transformingStreams.m
- (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 發(fā)生的變化直接綁定到 goButtonenabled 屬性上. 酷吧?對(duì) alpha 的綁定更酷, 因?yàn)槲覀冋谑褂?map 方法將值轉(zhuǎn)換成與 alpha 屬性相關(guān)的值. (注意在這里我們返回的是一個(gè) NSNumber 對(duì)象而不是原始float值. 這基本上是唯一的污點(diǎn): 你需要負(fù)責(zé)為 RAC 將原始值轉(zhuǎn)化為對(duì)象, 因?yàn)樗荒軒湍銓?dǎo)出來(lái).

多個(gè)訂閱者, 副作用, 昂貴的操作

訂閱信號(hào)鏈時(shí)要明白重要的一件事是每當(dāng)一個(gè)新值通過(guò)信號(hào)鏈被發(fā)送出去時(shí), 實(shí)際上會(huì)給每個(gè)訂閱者都發(fā)送一次. 直到意識(shí)到這就我們而言是有意義的, 信號(hào)發(fā)出的值不存儲(chǔ)在任何地方(除了 RAC 在內(nèi)部實(shí)現(xiàn)中). 當(dāng)信號(hào)需要發(fā)送一個(gè)新的值時(shí), 它會(huì)遍歷所有的訂閱者并給每個(gè)訂閱者發(fā)送那個(gè)值. (這是對(duì)信號(hào)鏈實(shí)際工作的簡(jiǎn)化說(shuō)明, 但基本想法是對(duì)的)

這為什么重要?這意味著信號(hào)鏈某處存在的任何副作用, 任何影響應(yīng)用世界的轉(zhuǎn)變, 將會(huì)發(fā)生多次. 這對(duì)新接觸 RAC 的用戶(hù)來(lái)說(shuō)是意想不到的. (這也違反了函數(shù)式構(gòu)建的理念-數(shù)據(jù)輸入, 數(shù)據(jù)輸出).

一個(gè)做作的例子可能是: 信號(hào)鏈某處的信號(hào)在每次按鈕被按下時(shí)更新 self 中的一個(gè)計(jì)數(shù)器屬性. 如果信號(hào)鏈有多個(gè)訂閱者, 計(jì)數(shù)器的增長(zhǎng)將會(huì)比你想的還要多. 你需要努力從信號(hào)鏈中盡可能剔除副作用. 當(dāng)副作用不可避免時(shí), 你可以使用一些恰當(dāng)?shù)念A(yù)防機(jī)制. 我將會(huì)在另一篇文章中探索.

除副作用之外, 你需要注意帶有昂貴操作和可變數(shù)據(jù)的信號(hào)鏈. 網(wǎng)絡(luò)請(qǐng)求就是一個(gè)三者兼得的例子:

  1. 網(wǎng)絡(luò)請(qǐng)求影響了應(yīng)用的網(wǎng)絡(luò)層(副作用).
  2. 網(wǎng)絡(luò)請(qǐng)求為信號(hào)鏈引入了可變數(shù)據(jù). (兩個(gè)完全一樣請(qǐng)求可能返回了不同的數(shù)據(jù). )
  3. 網(wǎng)絡(luò)請(qǐng)求反應(yīng)慢啊.

例如, 你可能有個(gè)信號(hào)在每次按鈕按下時(shí)發(fā)送一個(gè)值, 而你想將這個(gè)值轉(zhuǎn)換成網(wǎng)絡(luò)請(qǐng)求的結(jié)果. 如果有多個(gè)訂閱者要這個(gè)處理信號(hào)鏈上返回的這個(gè)值, 你將發(fā)起多個(gè)網(wǎng)絡(luò)請(qǐng)求.

signal-side-effect

網(wǎng)絡(luò)請(qǐng)求明顯是經(jīng)常需要的. 正如你所期望, RAC 提供這些情況的解決方案, 也就是 RACCommand 和多點(diǎn)廣播. 我將會(huì)在下一篇文章中更深入地分析.

Tweetboat Plus

既然簡(jiǎn)短的介紹(嗯哼)扯遠(yuǎn)了, 讓我們著眼于如何用 ReactiveCocoa 將 view-model 與視圖控制器連接起來(lái).

//
// 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);
}

讓我們過(guò)一遍這個(gè)例子.

RAC(self.viewModel, username) = [myTextfield rac_textSignal];

在這我們用 RAC 庫(kù)中的方法從 UITextField 拉取一個(gè)信號(hào). 這行代碼將 view-model 上的可讀寫(xiě)屬性 username 綁定到文本框上的用戶(hù)輸入的任何更新.

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)建了一個(gè)信號(hào) usernameIsValidSignal. 無(wú)論何時(shí)屬性發(fā)生變化, 它將會(huì)沿著管道發(fā)送一個(gè)新的 @YES 或 @NO. 我們拿到那個(gè)值并將其綁定到 goButton 的兩個(gè)屬性上. 首先我們將 alpha 分別對(duì)應(yīng) YES 或 NO 更新到1或0. 5(記著在這必須返回 NSNumber). 然后我們直接將信號(hào)綁定到 enabled 屬性, 因?yàn)?YES 和 NO 在這無(wú)需轉(zhuǎn)換就能完美地運(yùn)作.

RAC(self.avatarImageView,  image) = RACObserve(self.viewModel,  userAvatarImage);

RAC(self.userNameLabel,  text) = RACObserve(self.viewModel,  userFullName);

下面我們?yōu)楸眍^的圖像視圖和用戶(hù)標(biāo)簽創(chuàng)建綁定, 再次在 view-model 上對(duì)應(yīng)的屬性上用 RACObserve 宏創(chuàng)建信號(hào).

@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];
    }];

這貨看上去有點(diǎn)詭異, 所以我們?cè)谶@上多花點(diǎn)時(shí)間. 我們想在 view-model 上 tweets 數(shù)組或 allTweetsLoaded 屬性發(fā)生變化時(shí)更新表格視圖. (在這個(gè)例子中, 我們要用一個(gè)簡(jiǎn)單的方法來(lái)重新加載整張表. )所以我們將這兩個(gè)屬性被觀察后創(chuàng)建的兩個(gè)信號(hào)合并成一個(gè)更大的信號(hào), 當(dāng)兩個(gè)屬性中有一個(gè)發(fā)生變化, 這個(gè)信號(hào)就會(huì)發(fā)送值. (你一貫認(rèn)為信號(hào)的值是同類(lèi)型的, 不會(huì)像這個(gè)信號(hào)有一樣混雜的值. 這很可能在 Swift 版本的 RAC 中強(qiáng)制要求, 但在這我們不關(guān)心發(fā)出的真實(shí)值, 我們只是用它來(lái)觸發(fā)表格式圖的重新加載. )

那么這兒看起來(lái)最嚇人的部分可能是信號(hào)鏈中的 bufferWithTime: onScheduler: 方法. 需要它來(lái)圍繞 UIKit 中的一個(gè)問(wèn)題進(jìn)行變通. tweetsallTweetsLoaded 這兩個(gè)屬性我們都需要追蹤, 萬(wàn)一 tweets 變化和 allTweetsLoaded 為否(不管怎樣我們都得重新加載表格). 有時(shí)兩個(gè)屬性都將在同一準(zhǔn)確的時(shí)間發(fā)生變化, 意味著合并后的大信號(hào)中的兩個(gè)信號(hào)都會(huì)發(fā)送一個(gè)值, 那么 reloadData 方法將會(huì)在同一個(gè)運(yùn)行循環(huán)中被調(diào)用兩次. UIKit 不喜歡這樣. bufferWithTime: 在給明的時(shí)間內(nèi)抓取所有下一個(gè)到來(lái)的值, 當(dāng)給定的時(shí)間過(guò)后將所有值合在一起發(fā)給訂閱者. 通過(guò)傳入0作為時(shí)間, bufferWithTime: 將會(huì)抓取那個(gè)合并信號(hào)在特定的運(yùn)行循環(huán)中發(fā)出的全部值, 并將他們一起發(fā)送出去. (NSTimer 以同樣的方式工作, 這不是巧合, 因?yàn)?bufferWithTime: 是用 NSTimer 構(gòu)建的. )暫時(shí)不用擔(dān)心 scheduler, 試把它想做指明這些值必須在主線(xiàn)程上被發(fā)送. 現(xiàn)在我們確保 reloadData 每次運(yùn)行循環(huán)只被調(diào)用一次.
注意我在這用 @weakify/@strongify 宏切換 strong 和 weak. 這在創(chuàng)建所有這些 block 時(shí)非常重要. 在 RAC 的 block 中使用 self 時(shí)self 將會(huì)被捕獲為強(qiáng)引用并得到保留環(huán), 除非你尤其意識(shí)到要破除保留環(huán)

[[self.goButton rac_signalForControlEvents: UIControlEventTouchUpInside]
    subscribeNext:  ^(id value) {
        @strongify(self);
        [self.viewModel getTweetsForCurrentUsername];
    }];

我將會(huì)在下一篇文章中在這里引入 RACCommand, 但目前我們只是當(dāng)按鈕被觸碰時(shí)手動(dòng)調(diào)用 view-model 的 getTweetsForCurrentUsername 方法.
我們已經(jīng)搞定了 cellForRowAtIndexPath 的第一部分, 那么我在這將只說(shuō)下 loading cell:

MYLoadingCell *cell =
    [self.tableView dequeueReusableCellWithIdentifier: @"MYLoadingCell" forIndexPath: indexPath];
[self.viewModel loadMoreTweets];
return cell;

這是另一塊我們以后將利用到 RACCommand 的地方, 但目前我們只是調(diào)用 view-model 的 loadMoreTweets 方法. 我們將只是信任如果 cell 顯示或隱藏多次的話(huà) view-model 會(huì)避免多次內(nèi)部調(diào)用.

- (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);
}

這段現(xiàn)在應(yīng)該非常直接了, 除此之外我想指出一點(diǎn). 我們正在將圖片和文字綁定到 UI 上對(duì)應(yīng)的屬性, 但注意 viewModel 出現(xiàn)在 RACObserve 宏中逗號(hào)右邊. 這些 cell 終將被重用, 新的 view-models 將會(huì)被賦值. 如果我們不將 viewModel 放在逗號(hào)右邊, 那就會(huì)監(jiān)聽(tīng) viewModel 屬性的變化然后每次都要重新設(shè)置綁定;如果放在逗號(hào)右邊, RACObserve 將會(huì)為我們負(fù)責(zé)這些事兒. 因此我們只需要設(shè)定一次綁定并讓 Reactive Cocoa 做剩余的部分. 這是在綁定表格 cell 時(shí)為了性能需要記住的好東西. 我在實(shí)踐中即使是有很多表格 cell 依然沒(méi)有出過(guò)問(wèn)題.

福利-消除更多的狀態(tài)

有時(shí)候你可以在 view-model 中暴露 RACSignal 對(duì)象來(lái)替代像字符串和圖像這樣的屬性, 這能在 view-model 上消除更多的狀態(tài). 然后視圖控制器就不需要自己用 RACObserve 創(chuàng)建信號(hào)了, 并只是直接影響這些信號(hào). 要意識(shí)到如果你的信號(hào)在被 UI 訂閱/綁定到 UI 之前發(fā)出過(guò)一個(gè)值, 那么你將不會(huì)收到那個(gè)”初始”的值.

結(jié)論

本文篇幅略長(zhǎng), 但別被嚇著. 這還有好多沒(méi)講的, 而且是干貨兒, 是舒展你大腦的好方法. 這毫無(wú)疑問(wèn)是不同的編程風(fēng)格. 花一會(huì)兒功夫停止機(jī)械地試圖用命令式方案去解決問(wèn)題. 即使你一開(kāi)始不是經(jīng)常用這種編程風(fēng)格, 我認(rèn)為這有助于理解和提醒我們有截然不同的途徑來(lái)解決我們程序員的困惑.

下一次我將稍微深入 view-model 內(nèi)部中本文沒(méi)提到的內(nèi)容, 并介紹下 RACCommand(希望篇幅能短很多). 然后我們將投入到一個(gè)真實(shí)案例中, 那是我的一個(gè)叫做Three Cents的 app 中的一個(gè)相當(dāng)復(fù)雜的頁(yè)面, 它混合了網(wǎng)絡(luò)調(diào)用, CoreData, 多重 UI 狀態(tài), 等等!

ThreeCentsExplore

更多優(yōu)質(zhì)內(nèi)容歡迎大家關(guān)注我的個(gè)人博客: Mog

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末胃惜,一起剝皮案震驚了整個(gè)濱河市泞莉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌船殉,老刑警劉巖鲫趁,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異利虫,居然都是意外死亡挨厚,警方通過(guò)查閱死者的電腦和手機(jī)堡僻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)疫剃,“玉大人钉疫,你說(shuō)我怎么就攤上這事〕布郏” “怎么了牲阁?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)壤躲。 經(jīng)常有香客問(wèn)我城菊,道長(zhǎng),這世上最難降的妖魔是什么碉克? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任凌唬,我火速辦了婚禮,結(jié)果婚禮上棉胀,老公的妹妹穿的比我還像新娘。我一直安慰自己冀膝,他們只是感情好唁奢,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著窝剖,像睡著了一般麻掸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上赐纱,一...
    開(kāi)封第一講書(shū)人閱讀 49,111評(píng)論 1 285
  • 那天脊奋,我揣著相機(jī)與錄音,去河邊找鬼疙描。 笑死诚隙,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的起胰。 我是一名探鬼主播久又,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼效五!你這毒婦竟也來(lái)了地消?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤畏妖,失蹤者是張志新(化名)和其女友劉穎脉执,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體戒劫,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡半夷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年婆廊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片玻熙。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡否彩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出嗦随,到底是詐尸還是另有隱情列荔,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布枚尼,位于F島的核電站贴浙,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏署恍。R本人自食惡果不足惜崎溃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望盯质。 院中可真熱鬧袁串,春花似錦、人聲如沸呼巷。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)王悍。三九已至破镰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間压储,已是汗流浹背鲜漩。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留集惋,地道東北人孕似。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像刮刑,于是被迫代替她去往敵國(guó)和親鳞青。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

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