问题
在过去的几天里,我都在进行着 Gumroad's Small Product Lab 的挑战,就是使用Swift语言来开发一个Mac 应用。这个应用包含一个简单的 结构体 struct 类型 AppConfig, 表示应用中用户可以配置的选项。我所需要的就是创建一个ViewController 让用户可以编辑这些配置,也就是在这里我遇到了困难。
在Objective-C 中通常的做法是 使用 Cocoa Bindings来实现。这是一个构建在KVO之上的好功能,让你能够在Interface Builder 上自动地绑定你的UI元素到实例变量。
但是Cocoa Bindings 只作用于 NSObject 的子类上,我也想过直接把AppConfig 类型改为 NSObject,但把AppConfig作为一个 值类型是最佳的实践,而且我也不想因为这个小功能导入Objective-C的运行时。
由于我本来就是要用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,并实现了我们的协议,那么最终需要下面这样做:
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!
完美,现在开始写......
实现
考虑到观测对象有状态/生命周期,我决定把它作为类来对待:
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
}
}