プロパティへの代入時に KVC で変更を検出する
Cocoa/Swift プログラミング
KVC を使っていると、その変更を検出するには setValue:forKey: メソッドを使って書き込む必要があります。
ただ、プロパティに直接書き込むコードの方が読みやすいので、プロパティに直接書き込みされた時でも変更を検出できるようにしてみました。
キー値コーディングで変更を検出する
Objective-C では全てのクラスの起原にもなっているNSObjectクラス にはキー値コーディング (KVC) という仕組みを使って、それが持つプロパティに変更が加えられたときに、それを検出する仕組みが用意されています。
値の変化の検出する準備
簡単に紹介すると、たとえば次のようなクラスが定義されていたとします。
class MyClass : NSObject {
private(set) var building:Bool = false
}
このとき、次のようなコードを自身のクラス内で実行することで、自身のプロパティに変化が加えられたことを検出できるようになります。
self.addObserver(self, forKeyPath: "building", options: NSKeyValueObservingOptions.New, context: nil)
Xcode の Bindings Inspector を使って、プロパティの変化を検出し、それに連動してコントロールの状態を変化させることも可能です。
値の変化が検出されたとき
上記のforKeyPath
で指定したプロパティに変化があると、最初の引数で指定したオブジェクト(今回であれば自分自身)でそれを検出できます。
変化の検出は、次のメソッドを実装することで行います。
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
switch keyPath {
case "building":
break
default:
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}
値の変化の検出してもらう
ただ KVC の場合は、次のようにsetValue:forKey:メソッド を使ってプロパティを変更しないと、変化を検出してもらえない様子です。
self.setValue(NSNumber(bool: value), forKey: "building")
プロパティに直接値を書き込むよりもコードが読みにくくなる上に、今回のようにBool型 を値にしている場合は、例のようにNSNumberクラス でラップしてあげないといけないため、なおさらコードが読みづらくなります。
保存型プロパティに書き込んだときに変化を検出する
そこで、Swift 本来のプロパティ操作で KVC にも値の変化を検出してもらう方法を考えてみました。
KVC では値を直接書き換えるときに、書き換える直前にwillChangeValueForKey:メソッド を呼び出して、書き換えた直後にdidChangeValueForKey:メソッド を呼び出すことで、変更があったことを検出してもらえるようになっています。
これを Swift の保存型プロパティのwillSet
とdidSet
で実行すれば、プロパティの代入操作と合わせて KVC の変化の通知が行えます。
class MainWindowController: NSWindowController {
private(set) var building:Bool = false {
willSet {
self.willChangeValueForKey("building")
}
didSet {
self.didChangeValueForKey("building")
}
}
}
このように定義しておくことで、あとは次のように普通にプロパティ操作を行うだけで KVC での変化の検出が可能になります。
self.building = true
保存型プロパティに連動する計算型プロパティの変化も検出する
保存型プロパティの値を変更したら、それを使って計算している計算型プロパティの値も変化します。
この計算型プロパティの変化も KVC で検出できるようにしたいときには、保存型プロパティを書き換えたときに、関係する計算型プロパティの名前も変更対象として指定することで実現できる様子でした。
class MainWindowController: NSWindowController {
var canBuild:Bool {
return !self.building
}
private(set) var building:Bool = false {
willSet {
self.willChangeValueForKey("building")
self.willChangeValueForKey("canBuild")
}
didSet {
self.didChangeValueForKey("building")
self.didChangeValueForKey("canBuild")
}
}
}