KVOを利用する(Swift)
Jul 25, 2016 · iosswift3
SwiftでKVOを利用する方法について。特に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.'
といった実行時エラーが発生する
サンプルだと、value1とvalue2はprivateでも問題無いが、targetはprivateではエラーになる
StringやIntではエラーにならないのは確認したが、具体的な条件は未調査・・・
識別子としての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との相違点は、
KeyPathにselfがいらないoptionsを複数指定する時は、|ではなく配列(例:[.new, .old])で渡すcontextへのポインタは&だけで良い(暗黙的変換が入る)
といったあたり
通知の受信
今回は、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からの変更点
addObserverのcontextのポインタがUnsafeMutableRawPointerに変わっている
参考リンク
開発環境
- Xcode 8.2.1
