在過去的幾天裏,我都在進行着 Gumroad's Small Product Lab 的挑戰,就是使用Swift語言來開發一個Mac 應用。這個應用包含一個簡單的 結構體 struct 類型 AppConfig, 表示應用中用戶能夠配置的選項。我所須要的就是建立一個ViewController 讓用戶能夠編輯這些配置,也就是在這裏我遇到了困難。git
在Objective-C 中一般的作法是 使用 Cocoa Bindings來實現。這是一個構建在KVO之上的好功能,讓你可以在Interface Builder 上自動地綁定你的UI元素到實例變量。github
可是Cocoa Bindings 只做用於 NSObject 的子類上,我也想過直接把AppConfig 類型改成 NSObject,但把AppConfig做爲一個 值類型是最佳的實踐,並且我也不想由於這個小功能導入Objective-C的運行時。swift
因爲我原本就是要用Swift來進行挑戰這個項目的,因此我決定本身實現一個Swift版本的觀察者模式。數組
首先我須要定義一個協議,封裝了個人綁定操做:閉包
protocol ObservableProtocol { typealias T var value: T { get set } func subscribe(observer: AnyObject, block: (newValue: T, oldValue: T) -> ()) func unsubscribe(observer: AnyObject) }
假設咱們在一個對象類型的上下文中,這裏是self,並實現了咱們的協議,那麼最終須要下面這樣作:app
let initial = 3 var v = initial var obs = Observable(initial) obs.subscribe(self) { (newValue, oldValue) in print("Object updated!") v = newValue } obs.value = 4 // Trigger update. print(v) // 4!
完美,如今開始寫......ide
考慮到觀測對象有狀態/生命週期,我決定把它做爲類來對待:ui
public final class Observable<T>: ObservableProtocol { // ... }
咱們將開始定義一個變量以及其它方便使用的類型:翻譯
咱們的subscribers 模型就是一個個的觀察者,這是一個 ObserversEntry
元祖類型的數組,包括了一個監聽對象和一個閉包,這個閉包會在觀察的對象觸發時執行。咱們能夠經過 unsubscribe
方法來查找添加的觀察者,並移除特定的觀察者。
typealias ObserverBlock = (newValue: T, oldValue: T) -> () typealias ObserversEntry = (observer: AnyObject, block: ObserverBlock) private var observers: Array<ObserversEntry>
如今須要在咱們的類中實現init方法,默認的構造器只須要一個簡單的初始值,做爲咱們觀察到的初始值。同時這個構造器也須要初始化咱們的非可選的 observers
數組變量。
init(_ value: T) { self.value = value observers = [] }
同時咱們還要實現 didSet
方法,來在這個觀察值發生變化時通知咱們的觀察者:
var value: T { didSet { observers.forEach { (entry: ObserversEntry) in // oldValue is an implicit parameter to didSet in Swift! let (_, block) = entry block(newValue: value, oldValue: oldValue) } } }
最後,咱們須要實現 subscribe
和 unsubscribe
方法來添加和刪除咱們的觀察者:
func subscribe(observer: AnyObject, block: ObserverBlock) { let entry: ObserversEntry = (observer: observer, block: block) observers.append(entry) } func unsubscribe(observer: AnyObject) { let filtered = observers.filter { entry in let (owner, _) = entry return owner !== observer } observers = filtered }
注意:上面的實現只是最簡單的,並無考慮其它的異常狀況。
雖然上面的已經能夠正常工做了,可是我仍是想經過一些語法糖來減小重複編寫 foo.value = <value>
,因此我決定重載<<
這個符號。
func <<<T>(observable: Observable<T>, value: T) { observable.value = value }
更新: Chris Lattner ,Swift的發明者說了,建議我不要隨意重載已經存在的操做符號,因此若是你使用這個代碼,你能夠重載
<~
這個符號,或者其它類似的可是惟一的符號。
## 例子
/// A view controller supporting editing of the app's config. class PreferencesViewController: NSViewController { // The model layer. var configuration: ApplicationConfiguration // The view and object supporting the "controller". @IBOutlet var portTextField: NSTextField! var port: Observable<Int> // ... // MARK: NSViewController override func viewDidLoad() { super.viewDidLoad() port.subscribe(self) { (port, _) in // Ignore the old value, but update config with the new. self.configuration.port = port // You can trigger anything from here! Save to disk, etc... // Keeps action/UI code clean. } // ... // Assume `portTextFieldDidUpdate` is wired to be // called when portTextField's value updates. // ... } // MARK: Helpers func portTextFieldDidUpdate(value: Int) { port << value } }