Reactive Cocoa 3.0 在 MVVM 中的應用

nathanw發表於2015-08-28

這個是我最後一篇關於 ReactiveCocoa 3.0 (簡稱 RAC3)的文章,主要介紹了在 MVVM 實踐中更多複雜的 RAC 3.0 的用法。

(譯者注:前兩篇為《ReactiveCocoa 3.0 初見》和 《ReactiveCocoa 3.0 初見(2)》)

ReactiveCocoa 3.0 當前還處於測試階段,截止本文發表當天,已經有 4 個 beta 版。相較於 Objective-C 版本,3.0 版本帶來了全新的 Swift API。儘管函式響應式程式設計的核心理念保持不變,但是 Swfit API 相比於之前的版本卻有很大的不同。它使用了泛型,自定義操作符和柯里化函式,實現效果相當不錯。在我前面的幾個版本中,我探討了一般性的 Signal 類,它提供了強型別的訊號管道模型,還有一篇關於 SignalProducer 的類介面,SignalProducer 為具有附帶作用的訊號提供了一種更清晰的表達方式。

自從釋出這些文章,有很多人請求我演示更多複雜的 RAC3 程式碼例子,於是我就寫下這篇文章。

一個快速的 MVVM 回顧

ReactiveCocoa 本身並不是一個 MVVM 的框架,然而,它讓那些使用 MVVM 這種流行的 UI 架構的 app 變得更加容易構建。

這種模式的核心是 ViewModel。它是一個特殊型別的 model,反映 app 的 UI 狀態。它包含每個 UI 的詳細狀態,比如當前 textField 的文字內容,或者按鈕的 enable 狀態。它也提供了檢視可以進行的響應操作,比如響應按鈕的點選或者手勢識別的響應操作。

從 iOS 開發層面來具體看 MVVM 模式,View 由 ViewController 和對應的 UI 組成(無論是 nib,storyboard 還是程式碼生成)

使用 MVVM 會使得 View 層變得簡單,只需進行反映當前的 UI 狀態和其他一點點工作。

ReactiveCocoa 在 MVVM 應用中扮演者特殊的角色,提供一個簡單的途徑來同步檢視和它所關聯的 ViewModel。

在 REACTIVECOCOA 2.0 中進行 MVVM

在 RAC2,將 ViewModel 上的屬性繫結到檢視上通常需要使用一堆巨集:

上述程式碼通過巨集將 loadingIndicator 的 hidden 的屬性繫結到 ViewModel 的 executing signal 上。另一個有用的巨集是 RACObserve,可以從屬性中生成一個訊號,作用相當於一個對 KVO 的高效封裝。

(如果你之前沒有在 MVVM 中使用 ReactiveCocoa,你可以看看我之前的文章 tutorial on Ray Wenderlich’s site

不幸的是,RAC2 這些巨集使用起來有些笨拙和累贅。所有基於巨集的 API 都是如此,不單單只是 RAC 這些。

RAC3 不再使用巨集和 KVO,而是單純 Swift 風格的實現。

RAC3 屬性

幾個月前,我寫了一篇 KVO and a few KVO-alternatives with Swift,主要說的是缺少強型別,過度依賴 NSObject 和過於笨拙的語法,意味著 KVO 並不適合 swift 的世界。

在 RAC3,屬性(至少你要監聽的屬性)使用 MutableProperty 型別表示:

這個例項化了一個 name 屬性,型別是 String,初始值是空字串。注意 name 這個屬性是個常量(用的是 let),儘管它代表著一個可變的屬性。

可變屬性有一個很簡單的 API,一個 value 屬性和一個 put 方法,允許你 get 或 set 操作當前的值:

他們還暴露了一個型別為SignalProducerproducer 屬性,允許你觀察屬性的變化:

…每一步都很清晰,並且是強型別。

在 MVVM 模式,配置一個 ViewModel 通常都會繫結一個屬性。換句話說,就是確保檢視的變數屬性與對應的 ViewModel 屬性保持同步。

出於這個目的,RAC3 有個明確的操作符:

上述程式碼,viewModel 的 queryExecutionTime 繫結了 textField 的 rac_text 屬性。

注意: 當前 RAC3 不支援雙向繫結

一個 MVVM RAC3 的例子

作為約定,這篇部落格包括了一個更深入的 RAC3 的例子。一個 twitter 的搜尋 demo:

(對的,我的所有程式碼都與 Twitter 和 Flicker API 有關,如果你在天朝,自重)

這個 app 會根據給定的 text 來搜尋 tweet,搜尋會在使用者輸入後自動執行。

TwitterSearchViewModel 有如下這些屬性:

這些屬性就是所有 View 反映檢視 UI 狀態需要知道的屬性,並且允許通過 RAC3 繫結響應更新。tweet 的 tableView 通過 tweets 可變屬性被‘返回’,它包含一個陣列,裡面是 ViewModel 例項,每個都對應一個獨立的 cell。

TwitterSearchService 類是一個基於 RAC3 Twitter API 的封裝,用 signal producers 代表了網路請求。

這個應用的核心程式碼如下:

這個請求訪問了使用者 twitter 賬戶,接著管道將操作傳給 searchText.producer,也就是它觀察了它自己的 searchText 屬性。你將會注意到 producer 並沒有直接使用,而是先呼叫 map 方法 searchText.producer |> mapError。這在 RAC3 中是一個常見問題,因為 signal 有個 error 型別的常量,任何操作合併 signal (或者是 signal producers)就必須要求他們之間的 error 型別是相匹配的。使用 mapError 轉化 searchText.producer 可能產生的任意錯誤為一個 NSError,與管道中其他 signal 相匹配。

再往下,signal 被過濾(filter)和節流(throttle)。如果 searchText 多次變動,這可以減少 signal 的產生。

然後 signal 被 flat-mapped 到另一個 signal,根據給定的 text 搜尋 twitter。注意我已經打破了 flatMap 操作。這個是由於過載了原生的操作符,使得編譯器無法決定該執行那個。馬上我會在 Github 提一個 issue。

為 UIKit 新增可變屬性

當前 RAC 3 缺少一些 UIKit 整合,我猜想它會在下個 beta 版出現。現在,為了給檢視繫結一個 ViewModel,你不得不自己新增 extension。幸運的是這很簡單。

在另外一個專案,我已經建立了一個有用的函式,用來懶載入關聯屬性:

這裡建立了一個 T 型別的屬性,給定的比例函式式用於第一次訪問初始化屬性。

這個可以被用於懶載入一個可變屬性:

上面的程式碼建立了一個可變的屬性,然後向 producer 訂閱,當值改變的時候呼叫 setter 函式。

這個可以被用於新增 RAC3 屬性到檢視上,如下:

注意,這些屬性只有 getter 方法,你得通過屬性本身的 put 方法為他們賦值。

在這個專案我只新增了我需要的屬性:

對於控制元件來說,如果他們也會改變同樣的屬性,這種情況就自然會更復雜。在一個 textField 你必須訂閱使用者輸入結果的變化:

這裡,ViewModel 可以被繫結:

這裡還可以對繫結的值做 map 操作:

對於 tableView,這個專案還包括了 tableView 繫結程式碼的版本。

有趣的是,RAC3 還有個 ConstantProperty 類,看起來有點奇怪!我把它用在了構建每個 cell 的 ViewModel:

ConstantProperty 的值也是可以使用 <~ 繫結操作,而且在未來,如果你決定使它成為可變,你不需要在改變程式碼。

總結

RAC3 是一個快速發展的強大框架。也有一兩點缺陷,但是也不難看出它代表了一個里程碑。

所有 demo 的程式碼都可以在 Github 上找到。建議也看下另外一個 WhiskyNotebook 專案,它也用到了 RAC3。

相關文章