Xcode
iOS
Swift
swift4
ios11
OriginalゆめみDay 20

Swift4のKVOに新しいクロージャ関数が導入されました

はじめに

KVOとはKey-Value Observingの略で、Objective-Cの時代からあるCoCoaフレームワークの基本的な機能です。フレームワークとしてはFoundationに含まれ、Notificationsに分類されています。

Swift4から新しい関数やclassが導入されているようなので紹介いたします。

クロージャ記述

まず、KVOがクロージャで記述できるようになりました。
自分的にはKVOって何か複雑な感じがしていたのですが、クロージャに対応しただけで一気に分かりやすくなった気がします(笑)

Swift
func observe<Value>(_ keyPath: KeyPath<T, Value>, options: NSKeyValueObservingOptions = default, changeHandler: @escaping (T, NSKeyValueObservedChange<Value>) -> Void) -> NSKeyValueObservation

サンプル

例としてUIScrollViewのcontentOffsetに変化があった場合に設定されたクロージャを呼び出すコードを挙げます。

Swift
    // observeの結果を格納
    private var keyValueObservations = [NSKeyValueObservation]()

    // UIScrollViewの移動を感知してclosureを実行する
    private func addKVO(scrollView: UIScrollView, _ closure: @escaping ()->Void)
    {
        let keyValueObservation = scrollView.observe(\.contentOffset, options: [.new, .old])
        { _, change in
            if change.newValue == nil
            {
                return
            }
            closure() // キーボードを消すとか・・・
        }
        keyValueObservations.append(keyValueObservation)
    }

    // NSKeyValueObservation配列に格納したKVOを停止する
    private func removeKVO()
    {
        for keyValueObservation in keyValueObservations
        {
            keyValueObservation.invalidate()
        }
        keyValueObservations.removeAll()
    }

observeしたいオブジェクトに対してobserve関数を呼び出すだけです。プロパティの記述で\を入れているのはエスケープの意味なのかどうか分かりませんがAppleのソースではこのような記載になって居ます。
optionsは従来通り.newで新しい値を取得します。.oldをつけると変更前の値を取得できます。
クロージャ内の最初の引数が自分のインスタンスオブジェクトで、二番目が変更された値です。

NSKeyValueObservationクラスはほぼ説明がないのですが、コードのようにobserve処理を保持できるようです。 SDKではなく、Swiftで実装された便利クラスのようです。内部でaddObserver()を呼び出しobserveValueForKeyPath()の中でクロージャで指定されたブロックを呼び出しているのではないでしょうか。おそらくdeinitでremoveObserver()を呼び出しているためにNSKeyValueObservationを上位クラスでプロパティにして保持していれば、そのインスタンスが消える時に自動的にdeinitからremoveObserver()が呼び出される仕組みです。(このあたりは推測も入っています)

そもそもどこで使うか

KVOの使い所ですが、これは今回のobserve関数に限ったことではありません。
ですが、あえてここで書いておくと、KVOはプログラムコードの疎結合の実現するためにはかなり有効だと思います。

例えばUIScrollViewのdelegateプロパティは通常はArrayではないなので

Swift
var delegate: UIScrollViewDelegate?

オブサーバーインスタンスは1つに限られます。しかしこの制約はやっかいで、例えばサンプルのようなUIScrollViewの移動監視をライブラリとして分離したい場合は、メインのコード側が保持しているUIScrollViewインスタンスのdelegateを勝手にライブラリの中から使用するわけにはいきません。メインのコードが使用しているかもしれません。

そこでライブラリ側でKVOを使うとしたらどうでしょうか。
ライブラリ側にはUIScrollViewインスタンスを渡すだけで済みます。監視はライブラリ内でobserve関数を呼び出すだけです。

もともとはUIScrollViewのUIScrollViewDelegateのオブサーバーインスタンスをひとつしか登録できないのが問題なのですが、使いやすさを考慮するとこれは致し方ないでしょう。なのでもう一段階踏み込んでライブラリなどを整備していく場合はKVOを利用することは大きなメリットがあると考えています。

補足

解説記事がないか調べてみたのですが、2017年12月19日現在では

国内で見つかった記事は
【Swift 3.2以降】WKWebView の estimatedProgress の値監視に新しい Block ベースの KVO API を使ってみる
[iOS 11] Swift 4は前バージョンから何が変わったか比較した

Using Swift with Cocoa and Objective-C (Swift 4.0.3)
https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/AdoptingCocoaDesignPatterns.html
あたりには記述例が出ています。

またBuildできるサンプルとしては、
https://developer.apple.com/library/content/samplecode/AVCam/Introduction/Intro.html
があります。