KVOを利用する(Swift)

SwiftKVOを利用する方法について。特にcontextを一意の識別子として使いたい場合の方法

サンプルコード

ポイント

NSObjectを継承する

監視対象も監視するクラスも両方ともNSObjectのサブクラスであることが必要

監視対象(サンプルではTarget)で継承しなかった場合、

'NSUnknownKeyException', reason: '[<〜.ViewController 0x〜> addObserver:<〜.ViewController 0x〜> forKeyPath:@"target.valueA" options:3 context:0x〜] was sent to an object that is not KVC-compliant for the "target" property.'

といった実行時エラーが発生する

監視する側だとそもそもaddObserverなどが利用できない

プロパティにはdynamicをつける

監視対象のプロパティ(addObserverで追加するプロパティ)は必ずdynamicをつけること

もし、これをつけ忘れると、エラーにはならないが、通知も来ない状態 (=変更されてもobserveValueForKeyPathが呼ばれない) という判りにくいバグになってしまう

アクセスコントロールに注意

プロパティが別クラスのオブジェクト?の場合、privateにすると

'NSUnknownKeyException', reason: '[<〜.ViewController 0x〜> addObserver:<〜.ViewController 0x〜> forKeyPath:@"target.valueA" options:3 context:0x〜] was sent to an object that is not KVC-compliant for the "target" property.'

といった実行時エラーが発生する

サンプルだと、value1value2privateでも問題無いが、targetprivateではエラーになる

StringIntではエラーにならないのは確認したが、具体的な条件は未調査・・・

識別子としてのcontextの指定

通常の指定方法は参考リンクの通り(private var myContext = 0)。 ただ、今回のサンプルでは、キー値の指定とまとめて以下のようにしている

private struct KeyContext {
    static var value1 = "value1"
    ...
}

というのも、contextには一意なアドレスを渡すべきなので、staticによりアドレスを確保している (通常の指定方法ではグローバル変数にして一意なアドレスを確保)

なお、privateなのは単に他からアクセスさせないようにしたい(する必要がない)からで、 structの中で宣言しているのは、名前空間のようにしたかったからである。 よって、

private var value1 = "value1"
...

class ViewController: UIViewController {

と言った書き方でも同じ

通知の登録 / 解除

呼び出しタイミング

登録時

サンプルではUIViewControllerなので、viewWillAppearで登録しているが、 通常はinitでの登録が良さげ

解除時

サンプルでは登録がviewWillAppearなので、対となるviewWillDisappearで解除しているが、 通常はdeinitでの登録が良い

登録方法

addObserver(self, forKeyPath: KeyContext.value1, options: .new, context: &KeyContext.value1)

Objective-Cとの相違点は、

といったあたり

通知の受信

今回は、contextを識別子として利用しているので、switchでまとめて比較しているが、defalutの時にちゃんと

super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)

を呼ぶこと。これがないと、もし親クラスで何か監視をしていた場合に処理が正しく行われないので (当然、自身の監視対象だった場合は呼ばない)

なお、caseに監視対象のプロパティを書き忘れると、

An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.

の実行時エラーとなる

値の取得

// Change Dictionary Keys: 
// NSKeyValueChangeKey.newKeyとかNSKeyValueChangeKey.oldKeyとか
let value = change?["Change Dictionary Keys"] as? "データ型"

と書けば希望のデータ型へ変換して取得できる。 NSNullや型が違う場合などは、最終的にnilが入るのでサンプルのようにguardではじくのがスマート

Swift2からの変更点

addObservercontextのポインタがUnsafeMutableRawPointerに変わっている

参考リンク

開発環境