Edited at

Swift3ではKVOにkeyPath()式を使っていくのが便利

More than 1 year has passed since last update.


はじめに


  • 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)
}
}
}

このクラスPersonOfInterestPersonを監視する例は次の通りです

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()式を試してもコンパイルエラーになるはずです。