標籤:
原文 https://github.com/bboyfeiyu/iOS-tech-frontier/blob/master/issue-3/Swift的響應式編程.md
- 原文連結 : Reactive Swift
- 原文作者 : Agnes Vasarhelyi
- 譯文出自 : 開發技術前線 www.devtf.cn
- 譯者 :Mr.Simple
- 校對者:Lollypo
- 狀態 : 完成
讓我們首先回到Apple剛推出Objective-C的繼任者-Swift的時候,那真是一個非比尋常的時刻。 Siri還沒有開啟地獄之門,Prezi還沒有支援訂閱,那時的朝鮮也還沒有hack任何人的email。一種新語言的出現讓我個人非常興奮,尤其是這是一種型別安全的指令碼語言。雖然Swift還在快速的發展中,但是我們不必擔心它是否已經穩定。當那一刻到來之時,我應該已經知道如何交付整潔的、可測試的代碼。還要有非常靈活、流暢的UI?如果你對Objective-C 和 MVC之外的東西感興趣,那麼就請繼續讀下去吧。
MVC 與 MVVM
讓我們從零開始,當我們設計一個應用時,你可能會先考慮應用的架構。Cocoa架構以 Model-View-Controller (也稱為 MVC)為基礎架構,它的結構如所示。
雖然,你可能發現這個架構並沒有讓你的設計更有效,從中我們可以看到Controller角色的職責太過複雜。如果你有在iOS或者Mac上使用MVC的經驗,我打賭你已經領略到了View Controller承擔了過多的職責。你使用MVC的經驗越多,你就越會想找到另一個方法來解決掉MVC存在的這些問題。當使用MVC處理網路請求時,你會發現這會使得Controller變得極為臃腫,它不僅要處理擷取資料的請求,同時也要負責將這些資料渲染到UI上。更蛋疼的是你有嘗試過對一個ViewController進行單元測試嗎?不堪回首啊!這涉及到View、商務邏輯的測試。隨著View的複雜度的增長,它變得越來越難以維護。這就是為什麼人們不對MVC進行測試的原因。
Model-View-ViewModel (也叫MVVM)對於你的應用來說是一個更好的選擇。這是微軟發布的一個針對事件驅動實現的、非常強大的架構模式。正如你看到的,與MVC不同的是這裡的View持有ViewModel角色。
當然,這些都依賴於你想如何?這個模式,不管如何,最終MVVM模式都會給你帶來更低的複雜性、更好的可測試性。同時能達到這些效果嗎?怎麼可能?反正我相信了!它把所有的邏輯移到ViewModel角色中,以此來減少View Controller的職責。( 譯者注: 關於MVC、MVP、MVVM的區別請參考 這篇文章 。)
閃開,讓哥用一個項目來示範MVVM模式的使用,這是一個用來控制酒窖氣溫的半自動iOS用戶端項目。如何你想瞭解更多關於這個項目的細節,可以參考 這篇文章 。這個項目的關鍵在於我們能夠通過這個App看到酒窖中各個時間段的溫度。
讓我們回到MVVM與酒窖的溫度上。為了能夠讀取到溫度值,我們需要在我們的伺服器上使用WebSocket,使得我們基於MVC架構的應用能夠讀取它,並且將它顯示到iPhone上。首先我們需要建立一個Brew 模型類,這個類中有一個溫度欄位( temp ).
class Brew { var temp = 0.0}
我們還需要建立一個將酒窖溫度顯示到UI上的ViewController。
class BrewViewController: UIViewController { @IBOutlet weak var tempLabel: UILabel!}
通過MVC的方式,我們需要在ViewController中實現網路請求邏輯、更新UI的操作。
socket.on("temperature_changed", callback: {(AnyObject data) -> Void in self.brew.temp = data[0] as Float dispatch_async(dispatch_get_main_queue(), { self.updateTempLabel() })})
更新UI的函數 :
func updateTempLabel() { self.brewLabel.text = NSString(format:"%.2f ?C", self.brew.temp) }
此時,我們成功的把這些代碼都放到一個ViewController中了,非常好不是嗎?必須不是呐!
想象一下這種情境,當你需要先驗證使用者的身份、繪製一個即時的圖表或者處理出入的欄位時你如何??MVC架構的完整例子在這裡。但是現在,先讓我們來看看基於MVVM架構的實現:
var brew = Brew()dynamic var temp: Float = 0.0 { didSet { self.brew.temp = temp }}
上面就是儲存在ViewModel中的變數,雖然它們看起來有點陌生,但是我也不知道怎麼才能使它們看起來不那麼奇怪。不管它啦,我們先來看Socket:
socket.on("temperature_changed", callback: {(AnyObject data) -> Void in self.temp = data[0] as Float})
這樣,網路請求的邏輯就被移到了ViewModel中。但是我們如何修改UI呢? KVO 是我們的好朋友,我們就用它來實現,這裡是viewDidLoad中的一個程式碼片段 :
self.brewViewModel.addObserver(self, forKeyPath: "temp", options: .New, context: &brewContext)
我們需要觀察酒窖中的溫度,因此,我為View Model添加了一個單獨的欄位。你可能會問,為什麼我們要關心Brew模型是否在View Model中?我要說的是,Brew模型可能會有不同的欄位,而你可能需要將它序列化,通過網路傳輸它,或者其他的一些什麼操作。因此,我們需要它的狀態。
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) { if context == &brewContext { self.updateTempLabel((change[NSKeyValueChangeNewKey]) as Float) } else { super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context) } }
哇噢,這代碼太多了?KVO實現產生了很多的樣板代碼,但是它還是比MVC實現更為輕量級,尤其是當我們考慮添加越來越多的功能到我們的應用中時,這些邏輯就會被移到View Model中。這裡是MVVM實現的完整程式碼片段。
總之,MVVM實現好了很多,但是我們還有這還能再最佳化的感覺。現在,我們知道了展示可變資料模型到MVVM的View上的關鍵是資料繫結,既然這樣,那還有什麼比響應式編程適合這種情況?
響應式編程
“Reactive programming is programming with asynchronous data streams.” “響應式編程就是以非同步資料流的形式進行編程。”
如果你對這個概念還不熟悉,我建議你閱讀一下這篇文章 (中文版在這裡 : 那些年我們錯過的響應式編程 ),這是一篇非常好的介紹。在響應式編程的世界裡,任何東西都是流( stream )。下面這幅圖就展示了將一個按鈕點擊或者觸摸事件轉換為流。
如果運用響應式編程到我們的項目中,那麼情況應該是這樣的: 將socket資料看成是溫度的流,這個流被UILabel監聽著。轉換它們的流程也就是的函數式編程所展示的那樣,map、merge、filter等其他的操作,這個過程非常贊!讓我們來看看這些概念如何跟Cocoa一起配合。
ReactiveCocoa
當我們計劃在Cocoa上使用響應式編程時,最好的選擇就是使用ReactiveCocoa。ReactiveCocoa是一個受到 函數式編程 啟發的資料繫結架構。
它通過訊號來進行操作,這些訊號被定義為push-driven的流。這意味著,一個訊號代表了一個在未來會交付資料或者任意結果到它的觀察者的非同步工作流程。除了資料模型變化會產生一個訊號之外,ReactiveCocoa也提供了一些內建的UIKit/AppKit綁定,例如rac_textSignal和其他用用的東西。
你可能略微地不確定什麼時候才會計劃啟動你的下一個Swift項目,你可能也會想在這個項目中實現響應式編程。無需這樣,github上的開發人員會協助你實現這些功能。他們正在計劃發布名為Great Swiftening的ReactiveCocoa 3.0版本,當然你也可以使用2.4.x版本,因為它們也足夠穩定。
Swift在集合類型上也內建了一些函數式操作,它們能很方便的執行某些操作,我們無需對此進行重複地工作。
讓我們看看使用ReactiveCocoa作為MVVM模式的資料繫結系統時我們如何建立一個ReactiveViewModel。看如下代碼,我們的View Controller看起來是不是更優雅了?
self.brewViewModel.tempChangedSignal.map { (temp: AnyObject!) -> AnyObject! in return NSString(format:"%.2f ?C", temp as Float) } ~> RAC(self.tempLabel, "text") }
由於C式的複雜的宏在Swift中不可用,因此用於Objective-C的RAC宏被替換掉了。感謝Yusef Napora提供了一個直截了當的 解決方案 ,還有一種解決方案是在ColinEberhardt的這個Sample App中。
現在我們回到酒窖氣溫檢測項目中,我們已經準備好了運用響應式編程!我們看看剩下的代碼:
socket.on("temperature_changed", callback: {(AnyObject data) -> Void intempChangedSignal.sendNext(data[0]).deliverOn(RACScheduler.mainThreadScheduler()) })
在這裡,我們僅僅建立了一個溫度資料訊號,並且確保UILabel的更新會被執行在主線程中。我對於ReactiveCocoa簡直沒有抵抗力了!現在我們來執行http請求、處理當試圖在服務端初始化一個Brew對象時引發的錯誤,代碼如下 :
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue(), completionHandler: { (response:NSURLResponse!, data: NSData!, error: NSError!) -> Void in if error == nil { subscriber.sendNext(JSON(data)) subscriber.sendCompleted() } else { subscriber.sendError(error) }})
如果成功我們將接收到的Json資料發送給訂閱者,否則調用sendError函數將錯誤資訊發送給訂閱者。非常方便,不是嗎?
syncSignal.subscribeError({ (error: NSError!) -> Void in UIAlertView(title: "Error creating brew", message: error.localizedDescription, delegate: nil, cancelButtonTitle: "OK").show()})
我們僅僅是需要在ViewController中實現錯誤處理,然後等待著響應式編程的魔法降臨到我們身上,例如 丟失串連。
酒窖溫度監測項目是一個真實的樣本,起初我用Objective-C中的MVC實現.這種實現非常簡單,卻少了份驚喜.後來,Swift出現了,我理所當然地喜歡上這樣的創新.因此我決定重寫整個App.當我與繁重的控制器以及UI重新整理機制鬥爭了許久之後,這些東西我不是很精通,所以我決定嘗試響應式編程.由於我早已知道這與MVVM能夠相容,所以就決定嘗試一下.這次經曆清除了我一半代碼.於是,經過ReactiveCocoa轉換的App的價值感覺就像是剩下的代碼的價值的兩倍.從那時候起,做一些調整(比如根據一些資料改變按鈕的狀態)僅僅需要編寫一行用於發送啟用訊號到按鈕命令的代碼,這幾乎沒有成本.想想使用原來的方式將花費多少時間呐!
有很多其他的因素可能使你想嘗試ReactiveCocoa和Swift。如果你想對如何釀造啤酒感興趣,或者只是對這些代碼感興趣,那麼你可以這這裡(BrewMobile)查看到所有的代碼。如果你有更好的技術使得這個App變得更好,請讓我知道!當然,也歡迎給我們貢獻代碼。
在結束之前,分享一下我關於BrewMobile的未來的一些思考,我正在打算將React Native引入到BrewMobile。如果搞定了我會再次分享我的經驗
[譯] Swift 的響應式編程