ReactiveCocoa 和 MVVM 入門

MVC

任何一個正經開發過一陣子軟件的人都熟悉MVC. 它意思是Model View Controller, 是一個在複雜應用設計中組織代碼的公認模式. 它也被證明在 iOS 開發中有着第二種含義:  Massive View Controller(重量級視圖控制器). 它讓許多程序員絞盡腦汁如何去使代碼被解耦和組織地讓人滿意. 總的來講, iOS 開發者已經得出結論: 他們須要給視圖控制器瘦身, 並進一步分離事物;但該怎麼作呢? html

MVVM

因而MVVM流行起來, 它表明Model View View-Model, 它在這幫助咱們建立更易處理, 更佳設計的代碼. react

有時候違背蘋果建議的編碼方式並非個好作法. 我不是說不同意這樣子, 我指的是可能會弊大於利. 好比我不建議你去實現個本身的 view controller 基類並試着本身處理視圖生命週期. ios

帶着這種情緒, 我想提個問題: 使用除蘋果推薦的 MVC 以外的應用設計模式是愚蠢的麼?git

. 有兩個緣由. 程序員

  1. 蘋果沒有爲解決重量級試圖控制器問題提供真正的指導. 他們留給咱們來解決如何向代碼添加更多清晰的思路. 用 MVVM 來實現這個目的想必是極好噠. (在今年 WWDC 的一些視頻中, 蘋果工程師在屏幕上的示例代碼的確少量出現了 view-model, 不知道是否由於有它才成爲了示例代碼)
  2. MVVM, 至少是我將要在這裏展現的 MVVM 的風格, 都跟 MVC 十分兼容. 彷彿咱們將 MVC 進行到下一個邏輯步驟. 

我不會說起 MVC/MVVM 的歷史, 由於其餘地方已經有所介紹, 而且我也不精通. 我將會關注如何用它進行 iOS/Mac 開發. github

定義 MVVM

  1. Model - model 在 MVVM 中沒有真正的變化. 取決於你的偏好, 你的 model 可能會或可能不會封裝一些額外的業務邏輯工做. 我更傾向於把它當作一個容納表現數據-模型對象信息的結構體, 並在一個單獨的管理類中維護的建立/管理模型的統一邏輯. 
  2. View - view 包含實際 UI 自己(不管是 UIView 代碼, storyboard 和 xib), 任何視圖特定的邏輯, 和對用戶輸入的反饋. 在 iOS 中這不只須要 UIView 代碼和那些文件, 還包括不少需由 UIViewController 處理的工做. 
  3. View-Model - 這個術語自己會帶來困惑, 由於它混搭了兩個咱們已知的術語, 但倒是徹底不一樣的東東. 它不是傳統數據-模型結構中模型的意思(又來了, 只是我喜歡這個例子). 它的職責之一就是做爲一個表現視圖顯示自身所需數據的靜態模型;但它也有收集, 解釋和轉換那些數據的責任. 這留給了 view (controller) 一個更加清晰明確的任務: 呈現由 view-model 提供的數據. 

關於 view-model 的更多內容

view-model 一詞的確不能充分表達咱們的意圖. 一個更好的術語多是 「View Coordinator」(感謝Dave Lee提的這個 「View Coordinator」 術語, 真是個好點子). 你能夠認爲它就像是電視新聞主播背後的研究人員和做家團隊. 它從必要的資源(數據庫, 網絡服務調用, 等)中獲取原始數據, 運用邏輯, 並處理成 view (controller) 的展現數據. 它(一般經過屬性)暴露給視圖控制器須要知道的僅關於顯示視圖工做的信息(理想地你不會暴漏你的 data-model 對象). 它還負責對上游數據的修改(好比更新模型/數據庫, API POST 調用). objective-c

MVC 世界中的 MVVM

我認爲 MVVM 這個首字母縮寫如同 view-model 術語同樣, 對如何使用它們進行 iOS 開發體現得有點不太準確. 讓咱們再檢查下這個首字母縮寫, 瞭解下它是怎麼與 MVC 融爲一體的. shell

爲了圖解表示, 咱們顛倒了 MVC 中的 V 和 C, 因而首字母縮寫更能準確地反映出組件間的關係方位, 給咱們帶來 MCV. 我也會對 MVVM 這麼幹, 將 V(iew) 移到 VM 的右邊最終成爲了 MVMV. (我相信這些首字母縮寫起初不排成這樣更合理的順序是有緣由的. ) 數據庫

這是這兩種模式如何在 iOS 中組裝在一塊兒的簡單映射: 編程

  • 我試圖遵循區塊尺寸(很是)大體對應它們負責的工做量. 
  • 注意到視圖控制器有多大?
  • 你能夠看到咱們巨大的視圖控制器和 view-model 之間有大塊工做上的重合. 
  • 你也能夠看看視圖控制器在 MVVM 中的足跡有多大一部分是跟視圖重合的. 

你大可安心獲知咱們並無真的去除視圖控制器的概念或拋棄 「controller」 術語來匹配 MVVM. (唷. )咱們正要將重合的那塊工做剝離到 view-model 中, 並讓視圖控制器的生活更加簡單. 

咱們實際上最終以 MVMCV 了結. Model View-Model Controller View. 我確信我無拘無束的應用設計模式駭客行爲會讓人大吃一驚. 

咱們的結果: 

如今視圖控制器僅關注於用 view-model 的數據配置和管理各類各樣的視圖, 並在先關用戶輸入時讓 view-model 獲知並須要向上遊修改數據. 視圖控制器不須要了解關於網絡服務調用, Core Data, 模型對象等. (事實上有時經過 view-model 頭文件而不是複製一大堆屬性來暴漏 model 是很務實的, 後面還會有) 

view-model 會在視圖控制器上以一個屬性的方式存在. 視圖控制器知道 view-model 和它的公有屬性, 可是 view-model 對視圖控制器一無所知. 你早就該對這個設計感受好多了由於咱們的關注點在這兒進行更好地分離. 

幫助你理解咱們如何把組件組裝在一塊兒還有組件對應職責的另外一種方式, 就是着眼於咱們新的應用構建模塊層級圖. 

(感謝Dave Lee @kastiglione)

View-Model 和 View Controller, 在一塊兒,但獨立

咱們來看個簡單的 view-model 頭文件來對咱們新構件的長相有個更好地概念. 爲了情節簡單, 咱們構建按了一個僞造的推特客戶端來查看任何推特用戶的最新回覆, 經過輸入他們的姓名並點擊 「Go」. 咱們的樣例界面將會是這樣: 

  • 有一個讓用戶輸入他們姓名的 UITextField , 和一個寫着 「Go」 的 UIButton
  • 有顯示被查看的當前用戶頭像和姓名的 UIImageView 和 UILabel 各一個
  • 下面放着一個顯示最新回覆推文的 UITableView
  • 容許無限滾動

View-Model 實例

咱們的 view-model 頭文件應該長這樣: 

1
2
3
4
5
6
7
8
9
10
11
12
13
//MYTwitterLookupViewModel.h
@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-model 是如何得到這些信息的. 如今咱們二者都不在意. 僅僅假定你習慣於標準的網絡服務請求, 校驗, 數據操做和存儲. 

view-model 不作的事兒

  • 對視圖控制器以任何形式直接起做用或直接通告其變化

View Controller(視圖控制器)

視圖控制器從 view-model 獲取的數據將用來:

  • 當 usernameValid 的值發生變化時觸發 「Go」 按鈕的 enabled 屬性
  • 當 usernameValid 等於 NO 時調整按鈕的 alpha 值爲0. 5(等於 YES 時設爲1. 0)
  • 更新 UILable 的 text 屬性爲字符串 userFullName 的值
  • 更新 UIImageView 的 image 屬性爲 userAvatarImage 的值
  • 用 tweets 數組中的對象設置表格視圖中的 cell (後面會提到)
  • 當滑到表格視圖底部時若是 allTweetsLoaded 爲 NO, 提供一個 顯示 「loading」 的 cell

視圖控制器將對 view-model 起以下做用:

  • 每當 UITextField 中的文本發生變化, 更新 view-model 上僅有的 readwrite 屬性 username
  • 當 「Go」 按鈕被按下時調用 view-model 上的 getTweetsForCurrentUsername 方法
  • 當到達表格中的 「loading」 cell 時調用 view-model 上的 loadMoreTweets 方法

視圖控制器不作的事兒:

  • 發起網絡服務調用
  • 管理 tweets 數組
  • 斷定 username 內容是否有效
  • 將用戶的姓和名格式化爲全名
  • 下載用戶頭像並轉成 UIImage(若是你習慣在 UIImageView 上使用類別從網絡加載圖片, 你能夠暴漏 URL 而不是圖片. 這樣就給 view-model 與 UIKit 之間一個更清晰的劃分, 但我視 UIImage 爲數據而非數據的確切顯示. 這些東西不是固定死的. )
  • 流汗

請再次注意視圖控制器總的責任是處理 view-model 中的變化. 

子 View-Model

我提到過使用 view-model 上的 tweets 數組中的對象配置表格視圖的 cell.一般你會期待展示 tweets 的是數據-模型對象. 你可能已經對其感到奇怪, 由於咱們試圖經過 MVVM 模式不暴漏數據-模型對象. (前面提到過的) 

view-model 沒必要在屏幕上顯示全部東西. 你可用子 view-model 來表明屏幕上更小, 更潛在被封裝的部分. 若是一個視圖上的一小塊兒(好比表格的 cell)在 app 中能夠被重用以及(或)表現多個數據-模型對象, 子 view-model 會格外有利. 

你不老是須要子 view-model. 好比, 我可能用表格 header 視圖來渲染咱們「tweetboat plus」應用的頂部. 它不是個可重用的組件, 因此我可能僅是將咱們已經給視圖控制器用過的相同的 view-model 傳給那個自定義的 header 視圖. 它會用到 view-model 中它須要的信息, 而無視餘下的部分. 這對於保持子視圖同步是極好的方式, 由於它們能夠有效地與信息中相同確切的上下文做用, 並觀察確切相同屬性的更新. 

在咱們的例子中,  tweets 數組將會被下面這樣的子 view-model 充滿: 

1
2
3
4
5
6
//MyTweetCellViewModel.h
@interface MYTweetCellViewModel: NSObject

@property (nonatomic, strong, readonly) NSString *tweetAuthorFullName;
@property (nonatomic, strong, readonly) UIImage *tweetAuthorAvatarImage;
@property (nonatomic, strong, readonly) NSString *tweetContent;

你可能認爲這也太像普通」推特」裏的數據-模型對象了吧. 爲啥要干將其轉化成 view-model 的工做?即便相似, view-model 讓咱們限制信息只暴露給咱們須要的地方, 提供額外數據轉換的屬性, 或爲特定的視圖計算數據. (此外, 當能夠不暴露可變數據-模型對象時也是極好的, 由於咱們但願 view-model 本身承擔起更新它們的任務, 而不是靠視圖或視圖控制器. ) 

View-Model 從哪來?

那麼 view-model 是什麼時候何處被建立的呢?視圖控制器建立它們本身的 view-model 麼? 

View-Model 產生 View-Model

嚴格來講, 你應該爲 app delegate 中的頂級視圖控制器建立一個 view-model. 當展現一個新的視圖控制器時, 或很小的視圖被 view-model 表現時, 你應要求當前的 view-model 爲你建立一個子 view-model. 

加入咱們想要在用戶輕拍應用頂部的頭像時添加一個資料視圖控制器. 咱們能夠爲一級 view-model 添加相似以下方法: 

1
- (MYTwitterUserProfileViewModel *) viewModelForCurrentUser;

而後在咱們的一級視圖控制器中這麼用它: 

1
2
3
4
5
6
7
8
9
//MYMainViewController.m 
- (IBAction) didTapPrimaryUserAvatar
{
MYTwitterUserProfileViewModel *userProfileViewModel = [self.viewModel viewModelForCurrentUser];

MYTwitterUserProfileViewController *profileViewController =
[[MYTwitterUserProfileViewController alloc] initWithViewModel: userProfileViewModel];
[self.navigationController pushViewController: profileViewController animated:YES];
}

在這個例子中我將會展示當前用戶的資料視圖控制器, 可是個人資料視圖控制器須要一個 view-model. 我這的主視圖控制器不知道(也不應知道)用於建立關聯相關用戶 view-model 的所有必要數據, 因此它請求它本身的 view-model 來幹這種建立新 view-model 的苦差事. 

View-Model 列表

至於咱們的推特 cell, 當數據驅動屏幕(在這個例子中或許是經過網絡服務調用)聚到一塊兒時, 我將會表明性地提早爲對應的 cell 建立全部的 view-model. 因此在咱們這個方案中,  tweets 將會是一個 MYTweetCellViewModel 對象數組. 在個人表格視圖中的 cellForRowAtIndexPath 方法中, 我將會在正確的索引上簡單地抓取 view-model, 並把它賦值給個人 cell 上的 view-model 屬性. 

Functional Core, Imperative Shell

view-model 這種通往應用設計的方法是一塊應用設計之路上的墊腳石, 這種被稱做「Functional Core, Imperative Shell」的應用設計由Gary Bernhardt創造. (我最近十分有幸去聽Andy Matuschak關於這方面的演講, 他爲」胖的數值層, 瘦的對象層」提出充分理由. 雖然觀點類似, 但關注於咱們怎樣移除對象和它們狀態的邊界影響性質, 並用 Swift 中的新數據結構構建更加函數式, 可測試的數值層. )

Functional Core

view-model 就是 「functional core」, 儘管實際上在 iOS/Objective-C 中達到純函數水平是很棘手的(Swift 提供了一些附加的函數性, 這會讓咱們更接近). 大意是讓咱們的 view-model 儘量少的對剩餘的」應用世界」的依賴和影響. 那意味着什麼?想起你第一次學編程時可能學到的簡單函數吧. 它們可能接受一兩個參數並輸出一個結果. 數據輸入, 數據輸出.這個函數多是作一些數學運算或是將姓和名結合到一塊兒. 不管應用的其餘地方發生啥, 這個函數老是對相同的輸入產生相同的輸出. 這就是函數式方面. 

這就是咱們爲 view-model 謀求的東西. 他們富有邏輯和轉換數據並將結果存到屬性的功能. 理想上相同的輸入(好比網絡服務響應)將會導出相同的輸出(屬性的值). 這意味着儘量多地消除由」應用世界」剩餘部分帶來的可能影響輸出的因素, 好比使用一堆狀態. 一個好的第一步就是不要再 view-model 頭文件中引入 UIKit.h.(這是個重大原則, 但也有些灰色區域. 好比, 你可能認爲 UIImage 是數據而不是展現信息. PS: 我愛這麼幹. 既然這樣的話就得引入 UIKit. h 以便使用 UIImage 類)UIKit 其性質就是將要影響許多應用世界. 它包含不少」反作用」, 憑藉改變一個值或調用一個函數將觸發不少間接(甚至未知)的改變. 

更新: 剛剛看了 Andy 在函數式 Swift 會議上給出的另外一個超讚的演講, 因而又想到了一些. 要清楚你的 view-model 仍然只是一個對象, 而不用維護一些狀態(不然它將不會是你視圖中很是好用的模型了. )但你仍該努力將盡量多的邏輯移到無狀態的函數」值」中. 再重複一次, Swift在這方面比 Objective-C 更加可行. 

Imperative (Declarative?) Shell

命令式外殼 (Imperative Shell) 是咱們須要作全部的狀態轉換, 應用世界改變的苦差事的地方, 爲的是將 view-model 數據轉成給用戶在屏幕上看到的東西. 這是咱們的視圖(控制器), 實際上咱們在這分離 UIKit 的工做. 我仍將特別注意儘量消除狀態並用 ReactiveCocoa 這種陳述性質的東西作這方面工做, 而 iOS 和 UIKit 在設計上是命令式的. (表格的 data source 就是個很好的例子, 由於它的委託模式強制將狀態應用到委託中, 爲了當請求發生時可以爲表格視圖提供信息. 實際上委託模式一般強制一大堆狀態的使用)

可測試的核心

iOS 的單元測試是個髒, 苦, 亂的活兒. 至少我去作的時候得出的是這麼個結論. 就這方面我還出讀過一兩本書, 但當開始作視圖控制器的 mocking 和 swizzling 使其一些邏輯可測試時, 我目光呆滯. 我最終把單元測試納入模型和任何同類別模型管理類中. (譯者注: mock 是測試經常使用的手段, 而 method swizzling 是基於 Objective-C Runtime 交換方法實現的黑魔法) 

這個函數式核心同樣的 view-model 的最大優勢, 除了 bug 數量隨着狀態數遞減以外, 就是變得很是可以進行單元測試. 若是你有那種每次輸入相同而產生的輸出也相同的方法, 那就很是適合單元測試的世界. 咱們如今將咱們的數據用獲取/邏輯/轉換提取出, 避免了視圖控制器的複雜性. 那意味着構建棒棒噠測試時不須要用瘋狂的 mock 對象, method swizzling, 或其餘瘋癲的變通方法(但願能有). 

鏈接一切

那麼當 view-model 的共有屬性發生變化時咱們如何更新咱們的視圖呢?

絕大部分時間咱們用對應的 view-model 來初始化視圖控制器, 有點相似咱們剛剛在上文見到的: 

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

有時你沒法在初始化時將 view-model 傳入, 好比在 storyboard segue 或 cell dequeuing 的狀況下. 這時你應該在討論中的視圖(控制器)中暴露一個公有可寫的 view-model 屬性. 

1
2
3
4
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];

有時咱們能夠在鉤子程序調用前傳入 view-model, 好比 init 和 viewDidLoad, 咱們能夠從view-model 的屬性初始化全部 UI 元素的狀態. 

1
2
3
4
5
6
7
8
9
10
11
12
13
//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
}

好棒!咱們已經配置好了初始值. 當 view-model 上的數據改變時怎麼辦? 當」go」 按鈕在何時可用了怎麼辦?當用戶標籤和頭像在何時從網絡上下載並填充了怎麼辦? 

咱們能夠將視圖控制器暴露給 view-model, 以便於當相關數據變化或相似事件發送時它能夠調用一個 「updateUI」 方法. (別這麼幹. )在 view-model 上將視圖控制器做爲一個委託?當 view-model 內容有變化時發個通知?(不不不不. )

咱們的視圖控制器會感知一些變化的發生. 咱們可使用從 UITextfield 得來的委託方法在每當有字符變化時經過檢查 view-model 來更新按鈕的狀態. 

1
2
3
4
5
6
7
8
//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;
}

這種方法解決的場景是在只有再文本框發生變化時纔會影響 view-model 中的 isUsernameValid 值. 假使還有其餘變量/動做改變 isUsernameValid 的狀態將會怎麼樣?對於 view-model 中的網絡調用會怎麼樣?或許咱們該爲 view-model 上的方法加一個完成後回調處理, 這樣咱們此時就能夠更新 UI 的一切東西了?使用珍貴而笨重的 KVO 方法怎麼樣?

咱們或許最終使用多種多樣咱們熟悉的機制將 view-model 和視圖控制器全部的接觸點都連起來, 但你已經知道了標題上不是這麼寫的. 這樣在代碼中建立了大量的入口點, 僅僅爲了簡單的更新 UI 就要在代碼中徹底從新建立應用狀態上下文. 

進入 ReactiveCocoa

ReactiveCocoa(RAC) 是來拯救咱們的, 並剛好返回給咱們一點理智. 讓咱們看看如何作到. 

思考在一個新的用戶頁面上控制信息的流動, 當表單合法時更新提交按鈕的狀態. 你如今可能會照下面這麼作: 

你最後經過使用狀態, 當心翼翼地代碼中許多不一樣且零碎無關的內容穿到簡單的邏輯上. 看看你信息流中全部不一樣的入口點?(這還只是一個 UI 元素中的一條邏輯線. )咱們程序中如今用的抽象概念還不夠厲害, 不能爲咱們追蹤全部事物的關係, 因此咱們中止本身去幹這蛋疼事兒. 

讓咱們看看陳述版本: 

這看起來可能像是爲咱們應用流程文檔中的一張老舊的計算機科學圖解. 經過陳述式的編程, 咱們使用了更高層次的抽象, 來讓咱們實際編程更靠近咱們在腦海中設計流程的方式. 咱們讓電腦爲咱們作更多工做. 實際的代碼更加像這幅圖了. 

RACSignal

RACSignal (信號)就 RAC 來講是構造單元. 它表明咱們最終將要收到的信息. 當你能將將來某時刻收到的消息具體表示出來時, 你能夠開始預先(陳述性)運用邏輯並構建你的信息流,而不是必須等到事件發生(命令式). 

信號會爲了控制經過應用的信息流而得到全部這些異步方法(委託, 回調 block, 通知, KVO, target/action 事件觀察, 等)並將它們統一到一個接口下.這只是直觀理解. 不只是這些, 由於信息會流過你的應用, 它還提供給你輕鬆轉換/分解/合併/過濾信息的能力. 

那麼什麼是信號呢?這是一個信號:

信號是一個發送一連串值的物體. 可是咱們這兒的信號啥也不幹, 由於它尚未訂閱者. 若是有訂閱者監聽時(已訂閱)信號纔會發信息. 它將會向那個訂閱者發送0或多個載有數值的」next」事件, 後面跟着一個」complete」事件或一個」error」事件. (信號相似於其餘語言/工具包中的 「promise」, 但更強大, 由於它不只限於向它的訂閱者一次只傳遞一個返回值. ) 

正如我以前提到的, 若是以爲須要的話你能夠過濾, 轉換, 分解和合並那些值. 不一樣的訂閱者可能須要使用信號經過不一樣方式發送的值. 

信號發送的值是從哪得到的?

信號是一些等待某事發生的異步代碼, 而後把結果值發送給它們的訂閱者. 你能夠用 RACSignal 的類方法 createSignal: 手動建立信號: 

1
2
3
4
5
6
7
8
9
//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];
}];

我在這用一個具備成功和失敗 block (僞造)的網絡操做建立了一個信號. (若是我想讓信號在被訂閱時才讓網絡請求發生, 還能夠用 RACSignal 的類方法 defer. )我在成功的 block 裏使用提供的 subscriber 對象調用 sendNext: 和 sendCompleted:方法, 或在失敗的 block 中調用 sendError:. 如今我能夠訂閱這個信號並將在響應返回時接收到 json 值或是 error. 

幸運的是, RAC 的創造者實際上使用它們本身的庫來建立真的事物(捉摸一下), 因此對於咱們在平常須要什麼, 他們有很強烈的想法. 他們爲咱們提供了不少機制, 來從咱們一般使用的現存的異步模式中拉取信號. 別忘了若是你有一個沒有被某個內建信號覆蓋到的異步任務, 你能夠很容易地用 createSignal: 或相似方法來建立信號. 

一個被提供的機制就是 RACObserve() 宏. (若是你不喜歡宏, 你能夠簡單地看看罩子下面並用稍微多些冗雜的描述. 這也很是好. 在咱們獲得 Swift 版本的替代以前, 這也有在 Swift 中使用 RAC 的解決方案. )這個宏是 RAC 中對 KVO 中那些悲慘的 API 的替代. 你只須要傳入對象和你想觀察的那個對象某屬性的 keypath. 給出這些參數後,  RACObserve 會建立一個信號, 一旦它有了訂閱者, 它就馬上發送那個屬性的當前值, 並在發送那個屬性在這以後的任何變化. 

1
RACSignal *usernameValidSignal = RACObserve(self.viewModel, usernameIsValid);

這僅是提供用於建立信號的一個工具. 這裏有幾個當即可用的方式, 來從內置控制流機制中拉取信號: 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//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" }

記住你也能輕鬆建立本身的信號, 包括替代那些沒有內建支持的其餘委託. 咱們如今可以從全部這些不連貫的異步/控制流工具中拉取出信號並將他們合併, 試想一想這該多酷!這些會成爲咱們以前看到的陳述性圖表中的節點. 真是興奮. 

什麼是訂閱者?

簡言之, 訂閱者就是一段代碼, 它等待信號給它發送一些值, 而後訂閱者就能處理這些值了. (它也能夠做用於 「complete」 和 「error」 事件. )

這有一個簡單的訂閱者, 是經過向信號的實例方法 subscribeNext 傳入一個 block 來建立的. 咱們在這經過 RACObserve()宏建立信號來觀察一個對象上屬性的當前值, 並把它賦值給一個內部屬性. 

1
2
3
4
5
6
7
8
9
- (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 一般會幫你這些轉換. 

幸運的是 RAC 的創造者也意識到這種綁定行爲的廣泛必要性, 因此他們提供了另外一個宏 RAC(). 與 RACObserve() 相同, 你提供想要與即將到來的值綁定的對象和參數, 在其內部它所作的是建立一個訂閱者並更新其屬性的值. 咱們的例子如今看起來像這樣: 

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

考慮下咱們的目標, 這麼幹有點傻啊. 咱們不須要將信號發送的值存到屬性中(這會建立狀態), 咱們真正要作的是用從那個值獲取到信息來更新 UI. 

轉換數據流

如今咱們進入 RAC 爲咱們提供的用於轉換數值流的方法. 咱們將會利用 RACSignal 的實例方法 map

1
2
3
4
5
6
7
8
9
10
//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;
}];
}

這樣如今咱們將 view-model 上的 isUsernameValid 發生的變化直接綁定到 goButton 的 enabled 屬性上. 酷吧?對 alpha 的綁定更酷, 由於咱們正在使用 map 方法將值轉換成與 alpha 屬性相關的值. (注意在這裏咱們返回的是一個 NSNumber 對象而不是原始float值. 這基本上是惟一的污點: 你須要負責爲 RAC 將原始值轉化爲對象, 由於它不能幫你導出來. 

多個訂閱者, 反作用, 昂貴的操做

訂閱信號鏈時要明白重要的一件事是每當一個新值經過信號鏈被髮送出去時, 實際上會給每一個訂閱者都發送一次. 直到意識到這就咱們而言是有意義的, 信號發出的值不存儲在任何地方(除了 RAC 在內部實現中). 當信號須要發送一個新的值時, 它會遍歷全部的訂閱者並給每一個訂閱者發送那個值. (這是對信號鏈實際工做的簡化說明, 但基本想法是對的) 

這爲何重要?這意味着信號鏈某處存在的任何反作用, 任何影響應用世界的轉變, 將會發生屢次. 這對新接觸 RAC 的用戶來講是意想不到的. (這也違反了函數式構建的理念-數據輸入, 數據輸出). 

一個作做的例子多是: 信號鏈某處的信號在每次按鈕被按下時更新 self 中的一個計數器屬性. 若是信號鏈有多個訂閱者, 計數器的增加將會比你想的還要多. 你須要努力從信號鏈中儘量剔除反作用. 當反作用不可避免時, 你可使用一些恰當的預防機制. 我將會在另外一篇文章中探索. 

除反作用以外, 你須要注意帶有昂貴操做和可變數據的信號鏈. 網絡請求就是一個三者兼得的例子: 

  1. 網絡請求影響了應用的網絡層(反作用). 
  2. 網絡請求爲信號鏈引入了可變數據. (兩個徹底同樣請求可能返回了不一樣的數據. )
  3. 網絡請求反應慢啊. 

例如, 你可能有個信號在每次按鈕按下時發送一個值, 而你想將這個值轉換成網絡請求的結果. 若是有多個訂閱者要這個處理信號鏈上返回的這個值, 你將發起多個網絡請求. 

網絡請求明顯是常常須要的. 正如你所指望, RAC 提供這些狀況的解決方案, 也就是 RACCommand 和多點廣播. 我將會在下一篇文章中更深刻地分析. 

Tweetboat Plus

既然簡短的介紹(嗯哼)扯遠了, 讓咱們着眼於如何用 ReactiveCocoa 將 view-model 與視圖控制器鏈接起來. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//
// 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);
}

讓咱們過一遍這個例子. 

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

在這咱們用 RAC 庫中的方法從 UITextField 拉取一個信號. 這行代碼將 view-model 上的可讀寫屬性 username 綁定到文本框上的用戶輸入的任何更新. 

1
2
3
4
5
6
7
8
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 屬性上建立了一個信號 usernameIsValidSignal. 不管什麼時候屬性發生變化, 它將會沿着管道發送一個新的 @YES 或 @NO. 咱們拿到那個值並將其綁定到 goButton 的兩個屬性上. 首先咱們將 alpha 分別對應 YES 或 NO 更新到1或0. 5(記着在這必須返回 NSNumber). 而後咱們直接將信號綁定到 enabled 屬性, 由於 YES 和 NO 在這無需轉換就能完美地運做. 

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

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

下面咱們爲表頭的圖像視圖和用戶標籤建立綁定, 再次在 view-model 上對應的屬性上用 RACObserve 宏建立信號. 

1
2
3
4
5
6
7
8
@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];
}];

這貨看上去有點詭異, 因此咱們在這上多花點時間. 咱們想在 view-model 上 tweets 數組或 allTweetsLoaded 屬性發生變化時更新表格視圖. (在這個例子中, 咱們要用一個簡單的方法來從新加載整張表. )因此咱們將這兩個屬性被觀察後建立的兩個信號合併成一個更大的信號, 當兩個屬性中有一個發生變化, 這個信號就會發送值. (你一向認爲信號的值是同類型的, 不會像這個信號有同樣混雜的值. 這極可能在 Swift 版本的 RAC 中強制要求, 但在這咱們不關心發出的真實值, 咱們只是用它來觸發表格式圖的從新加載. ) 

那麼這兒看起來最嚇人的部分多是信號鏈中的 bufferWithTime: onScheduler: 方法. 須要它來圍繞 UIKit 中的一個問題進行變通.  tweets 和 allTweetsLoaded 這兩個屬性咱們都須要追蹤, 萬一 tweets 變化和 allTweetsLoaded 爲否(無論怎樣咱們都得從新加載表格). 有時兩個屬性都將在同一準確的時間發生變化, 意味着合併後的大信號中的兩個信號都會發送一個值, 那麼 reloadData 方法將會在同一個運行循環中被調用兩次. UIKit 不喜歡這樣.  bufferWithTime: 在給明的時間內抓取全部下一個到來的值, 當給定的時間事後將全部值合在一塊兒發給訂閱者. 經過傳入0做爲時間,  bufferWithTime: 將會抓取那個合併信號在特定的運行循環中發出的所有值, 並將他們一塊兒發送出去. (NSTimer 以一樣的方式工做, 這不是巧合, 由於 bufferWithTime: 是用 NSTimer 構建的. )暫時不用擔憂 scheduler, 試把它想作指明這些值必須在主線程上被髮送. 如今咱們確保 reloadData 每次運行循環只被調用一次. 

注意我在這用 @weakify/@strongify 宏切換 strong 和 weak. 這在建立全部這些 block 時很是重要. 在 RAC 的 block 中使用 self 時self 將會被捕獲爲強引用並獲得保留環, 除非你尤爲意識到要破除保留環

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

我將會在下一篇文章中在這裏引入 RACCommand, 但目前咱們只是當按鈕被觸碰時手動調用 view-model 的 getTweetsForCurrentUsername 方法. 

咱們已經搞定了 cellForRowAtIndexPath 的第一部分, 那麼我在這將只說下 loading cell: 

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

這是另外一塊咱們之後將利用到 RACCommand 的地方, 但目前咱們只是調用 view-model 的 loadMoreTweets 方法. 咱們將只是信任若是 cell 顯示或隱藏屢次的話 view-model 會避免屢次內部調用. 

1
2
3
4
5
6
7
- (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);
}

這段如今應該很是直接了, 除此以外我想指出一點. 咱們正在將圖片和文字綁定到 UI 上對應的屬性, 但注意 viewModel 出如今 RACObserve 宏中逗號右邊. 這些 cell 終將被重用, 新的 view-models 將會被賦值. 若是咱們不將 viewModel 放在逗號右邊, 那就會監聽 viewModel 屬性的變化而後每次都要從新設置綁定;若是放在逗號右邊,  RACObserve 將會爲咱們負責這些事兒. 所以咱們只須要設定一次綁定並讓 Reactive Cocoa 作剩餘的部分. 這是在綁定表格 cell 時爲了性能須要記住的好東西. 我在實踐中即便是有不少表格 cell 依然沒有出過問題. 

福利-消除更多的狀態

有時候你能夠在 view-model 中暴露 RACSignal 對象來替代像字符串和圖像這樣的屬性, 這能在 view-model 上消除更多的狀態. 而後視圖控制器就不須要本身用 RACObserve 建立信號了, 並只是直接影響這些信號. 要意識到若是你的信號在被 UI 訂閱/綁定到 UI 以前發出過一個值, 那麼你將不會收到那個」初始」的值. 

結論

本文篇幅略長, 但別被嚇着. 這還有好多沒講的, 並且是乾貨兒, 是舒展你大腦的好方法. 這毫無疑問是不一樣的編程風格. 花一下子功夫中止機械地試圖用命令式方案去解決問題. 即便你一開始不是常常用這種編程風格, 我認爲這有助於理解和提醒咱們有大相徑庭的途徑來解決咱們程序員的困惑. 

下一次我將稍微深刻 view-model 內部中本文沒提到的內容, 並介紹下 RACCommand(但願篇幅能短不少). 而後咱們將投入到一個真實案例中, 那是個人一個叫作Three Cents的 app 中的一個至關複雜的頁面, 它混合了網絡調用, CoreData, 多重 UI 狀態, 等等!

拓展閱讀

相關文章
相關標籤/搜索