はじめに
- KVO使う際は文字列でプロパティのkeyPathを指定しなくていいですよ
- 文字列指定だとプロパティ名が変わった時とか実行時になって間違いに気づいてヤバい
- Swift 3からの#keyPath()式を使うことで静的にチェックできるのでコンパイル時に気づけます
KVOとSwift 3でググったときにシンプルな利用例が無いので記載しておきます。
具体例で#keyPath()
具体的に例を上げて#keyPath()
の使い方を書いておきます。Person
クラスのage
プロパティをPersonOfInterest
クラスが監視する例です。
まず、監視されるクラスPerson
の定義
class Person: NSObject {
dynamic var age: NSNumber
init(age: NSNumber) {
self.age = age
}
}
これは年齢ageが変えられるようにvar
にしておきます。
その年齢を監視するのでdynamic
キーワードを使用します。dynamicについてはここを読むと分かりやすいでしょう。
その上で、このageの文字列があれば監視できるので、そのために#keyPath(Person.age)
という使い方をします。これによって文字列としてageを取得できます。
print(#keyPath(Person.age)) // age
#keyPath()
式で指定する際に重要なのは
-
#keyPath(Person.age)
のクラス名.プロパティ名が正しい -
#keyPath(インスタンス名.age)
だと駄目
当然Person.ageがTypoしていればコンパイルエラーになります。
次にPerson
を監視するクラスPersonOfInterest
を定義。age
を監視するメソッドobserveValue
を用意します。
final class PersonOfInterest: NSObject {
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
guard let keyPath = keyPath, let change = change else { return }
if keyPath == #keyPath(Person.age) {
guard let old = change[NSKeyValueChangeKey.oldKey] as? NSNumber else { return }
guard let current = change[NSKeyValueChangeKey.newKey] as? NSNumber else { return }
print(old, current)
}
}
}
このクラスPersonOfInterest
が Person
を監視する例は次の通りです
let poi = PersonOfInterest()
let chris = Person(age: NSNumber(value: 10))
chris.addObserver(poi, forKeyPath: #keyPath(Person.age), options: [.old, .new], context: nil)
chris.age = NSNumber(value: 11) // old:10, current:11
chris.age = NSNumber(value: 11) // old:11, current:11 値は変わってないがオブジェクト変わってるから
chris.age = NSNumber(value: 20) // old:11, current:20
chris.removeObserver(poi, forKeyPath: #keyPath(Person.age))
chris.age = NSNumber(value: 30) // 監視を辞めた後なので反応なし
監視して値を出力すると、old, currentの並びで次のような値になりました。
10, 11
11, 11
11, 20
監視をやめた後に30に変更した際には反応していないのと、"11"に変えた後にまた"11"に変えても監視が呼び出されています。これは文字列のオブジェクト自体の再代入が起こっているので変化があったということでしょう。
おわりに
#keyPath()
式自体を利用して文字列でプロパティ名を取得するためには、今回のPersonのように、監視対象のクラスがNSObjectを継承している必要があります。そもそもaddObserverメソッドがNSObjectを継承する必要があるので忘れることはないでしょうが、SwiftのObjectで#KeyPath()
式を試してもコンパイルエラーになるはずです。