Open5
SwiftのKVO(Key Value Observing)のまとめ
このスクラップを作り始めたきっかけ
- WKWebViewの
estimatedProgress
とisLoading
の値変更を検知したい場合に、KVOが使用されている場合が100%だった(私の観測範囲では)ので、こいつが何者か調べてみた
KVO(Key Value Observing)とは
- 公式ドキュメントによると、以下の通り。
他のオブジェクトのプロパティの変化を、別のオブジェクトに伝えるための使われるプログラミングパターン
例えばModelとView間
- 以下は公式ドキュメントを参考にPlaygroundで実装
import UIKit
class MyObjectToObserve: NSObject {
@objc dynamic var myDate = NSDate(timeIntervalSince1970: 0) // 1970
func updateDate() {
myDate = myDate.addingTimeInterval(Double(2 << 30)) // 68年プラスする
}
}
class MyObserver: NSObject {
@objc var objectToObserve: MyObjectToObserve
var observation: NSKeyValueObservation?
init(object: MyObjectToObserve) {
objectToObserve = object
super.init()
observation = observe(
\.objectToObserve.myDate,
options: [.old, .new] // 変更前と変更後の値を監視する。必要なければなくても問題ない
) { object, change in
print("myDate changed from: \(change.oldValue!), updated to: \(change.newValue!)")
}
}
}
let observed = MyObjectToObserve()
let observer = MyObserver(object: observed)
// 値を変更する
observed.updateDate()
- コード全体はこちら
使用シーン
-
値の変更を監視したいとき、かつ監視対象側に監視するための方法が存在しないかつ自分で実装することもできない場合
- 値の変更を監視する方法は、現時点のiOS開発では他にもより良い方法がある
- e.g. Delegateパターン
- だが、例えばDelegateパターンでは実現できないケースもある
- e.g.
AVFoundation
のような一般的なクラスを扱う場合で、プレーヤーの状態(再生/一時停止など)を監視したい場合、AVPlayerItem
のソースコードを変更することはできないので、KVOを使用するしかない - おそらく、
WKWebView
のestimatedProgress
(ローディングがどこまで完了しているか)やisLoading
(ローディング中かどうか)は、このケースに当てはまっている
- e.g.
- 値の変更を監視する方法は、現時点のiOS開発では他にもより良い方法がある
-
参考
Tips
-
NSKeyValueObservation.invalidate()
は極力避ける- マルチスレッド環境で、
NSRangeException
を起こすから -
observer.observation
へnilをアサインして解放すれば、例外は起こらない - NSKeyValueObservationを扱うときは、何かしらの理由で、
let
で宣言しない限りは、invalidate()
を呼ばす、単に解放してあげる方法が安全
- マルチスレッド環境で、
-
NSKeyValueObservation
は、nullableで保持し、インスタンス解放で無効化する
deinit {
observation.invalidate()
}
- 参考
マメ知識
-
@objc
やNS
から始まるものが多いことからもわかるとおり、objective-c時代から存在しているよう - Swift4以降では、KVOの実装の仕方が新しくなっている
古い書き方
class ViewController: UIViewController {
let player = AVPlayer()
override func viewDidLoad() {
super.viewDidLoad()
/// 1. Add the observer for AVPlayerItem status change
player.currentItem?.addObserver(self,
forKeyPath: #keyPath(AVPlayerItem.status), /// New syntax for adding keyPath so you can utilize the compiler for syntax suggestion
options: [.old, .new],
context: nil)
}
/// 2. The notification is handled here.
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(AVPlayerItem.status) {
let status: AVPlayerItem.Status
if let statusNumber = change?[.newKey] as? NSNumber {
status = AVPlayerItem.Status(rawValue: statusNumber.intValue)!
} else {
status = .unknown
}
// Switch over status value
switch status {
case .readyToPlay:
// Player item is ready to play.
break
case .failed:
// Player item failed. See error.
break
case .unknown:
// Player item is not yet ready.
break
}
}
}
deinit {
/// 2. And you must remember to remove it to avoid crashing
player.currentItem?.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.status))
}
}
新しい書き方
class ViewController: UIViewController {
let player = AVPlayer()
var timeControlStatusObserver: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
/// 1. Add the observer for AVPlayerItem status change. And that's it.
timeControlStatusObserver = player.observe(\.timeControlStatus, options: [.old, .new]) { (player, change) in
if let newValue = change.newValue,
let oldValue = change.oldValue,
newValue != oldValue {
print("Time control status has changed")
}
}
}
deinit {
/// Nothing needs to be done. Make sure this view controller can released (no retain cycle) and the observation will be removed when it is deinited.
}
}
- 参考