Open5

SwiftのKVO(Key Value Observing)のまとめ

kamimikamimi

このスクラップを作り始めたきっかけ

  • WKWebViewのestimatedProgressisLoadingの値変更を検知したい場合に、KVOが使用されている場合が100%だった(私の観測範囲では)ので、こいつが何者か調べてみた
kamimikamimi

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()
  • コード全体はこちら

https://github.com/kamimi01/iOSTroubleShooting/tree/main/UsingKey-ValueObservingInSwift.playground

kamimikamimi

使用シーン

  • 値の変更を監視したいとき、かつ監視対象側に監視するための方法が存在しないかつ自分で実装することもできない場合

    • 値の変更を監視する方法は、現時点のiOS開発では他にもより良い方法がある
      • e.g. Delegateパターン
    • だが、例えばDelegateパターンでは実現できないケースもある
      • e.g. AVFoundationのような一般的なクラスを扱う場合で、プレーヤーの状態(再生/一時停止など)を監視したい場合、AVPlayerItemのソースコードを変更することはできないので、KVOを使用するしかない
      • おそらく、WKWebViewestimatedProgress(ローディングがどこまで完了しているか)やisLoading(ローディング中かどうか)は、このケースに当てはまっている
  • 参考

https://medium.com/nerd-for-tech/key-value-observing-in-the-age-of-swift-f509c54fa5e8

kamimikamimi

Tips

  • NSKeyValueObservation.invalidate()は極力避ける
    • マルチスレッド環境で、NSRangeExceptionを起こすから
    • observer.observationへnilをアサインして解放すれば、例外は起こらない
    • NSKeyValueObservationを扱うときは、何かしらの理由で、letで宣言しない限りは、invalidate()を呼ばす、単に解放してあげる方法が安全
  • NSKeyValueObservationは、nullableで保持し、インスタンス解放で無効化する
deinit {
  observation.invalidate()
}
  • 参考

https://qiita.com/SCENEE/items/4cd79cf650dd6c44c2f5

https://www.reddit.com/r/swift/comments/cr84wc/how_to_properly_remove_kvo_observer/

kamimikamimi

マメ知識

  • @objcNSから始まるものが多いことからもわかるとおり、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.
    }
    
}
  • 参考

https://medium.com/nerd-for-tech/key-value-observing-in-the-age-of-swift-f509c54fa5e8

https://nalexn.github.io/kvo-guide-for-key-value-observing/