LoginSignup
8
5

More than 3 years have passed since last update.

SwiftでModel の変更を監視

Last updated at Posted at 2016-11-17

MVCでImageを検索してみると、想像している以上にいろいろな、MVCの図に遭遇します。どの図がいいとかの話は置いておくとして、Model に変更があった場合、何らかの方法で View または ViewController はその変更を検知して表示に反映させたいものです。

wikipedia
SAP

今回は、このモデルの変更の通知を簡単にする為に、軽量のユーティリティコードを開発したので紹介したいと思います。

ZObservable

swift license

ZObservable のコードは以下の github より入手可能になっています。

ZObservable と ZObserver

ZObservable は監視される側の ZObservable と、監視する側 ZObserver の基本構成としています。どちらも protocol ですが、監視する側も監視される側もオブジェクトでなくてはなりません。

まず監視される側の典型的なコードは以下のようになります。この例では、image に変更があった場合は、self.observableDidChange() を呼び出します。image の他にも監視対象下に置きたいプロパティなどがあれば、変更があった際に self.observableDidChange() を呼びます。そうです、監視と言いながら、通知は監視される側の自己申告です。また、KVO と違い、どのオブジェクトのどのプロパティに変更があったという監視ではなく、あるオブジェクトの何かが変わったという情報の粒度での監視を行います。再表示が必要である事を監視する程度であれば、この程度の情報の粒度で十分であるかと思います。

class MyObservable: ZObservable { // 監視される側
    var image: UIImage? {
        didSet {
            self.observableDidChange()
        }
    }
}

そして、監視する側は func observableDidChange(_ observable: ZObservable) を実装する必要があります。監視対象のオブジェクトに変更があった場合はこのメソッドが呼ばれるからです。また、Observer は複数の Observable を監視対象化におく事ができるので、必要に応じて、switch 文などで、タイプ別、または インスタンス別に処理を切り分ける事ができます。

class MyObserver: ZObserver { // 監視する側
    func observableDidChange(_ observable: ZObservable) {
        switch observable {
        case let object as MyObservable:
            print("\(object) did change.")
        default:
            break
        }
    }
}

これで監視する側と監視される側のクラスの準備はできました。次はこの二つをつなぎ合わせます。

let observable = MyObservable()
let observer = MyObserver()
observable.addObserver(observer)

この状態で、Observable の監視の対象になっているプロパティを変更すると、Observer側 の observableDidChange() が呼ばれます。

observable.image = UIImage(named: "...") // "MyObservable did change."

そして監視不要になった場合は、Observable から removeObserver() で監視対象から外す事ができますが、監視する側も監視される側も弱参照(weak reference)で登録されている為、どちらか、もしくは、その両方のオブジェクトがリリースされた場合は自動的に監視対象から外れ、通知はされなくなります。

observable.removeObserver(observer)

次のようなコードで確認する事ができます。observer がスコープを抜けた時点以降通知されていない事に注意ください。

class YourObservable: ZObservable { // 監視される側
    var value: Int = 0 {
        didSet {
            self.observableDidChange()
        }
    }
}

class YourObserver: ZObserver { // 監視する側
    func observableDidChange(_ observable: ZObservable) {
        switch observable {
        case let object as YourObservable:
            print("value did change to `\(object.value)`")
        default:
            break
        }
    }
}

let observable = YourObservable()

do {
    let observer = YourObserver()
    observable.addObserver(observer)
    observable.value = 1 // "value did change to `1`"
}

observable.value = 2 // 通知されない

課題

ZObservable は単なるプロトコルなので protocol extension を使えば、多くのクラスを Observable にする事ができます。例えば NSMutableURLRequest を protocol extension で ZObservable にしてしまえば、マニュアルでですが、observableDidChange() を呼び出して、通知を行う事ができます。

extension NSMutableURLRequest: ZObservable {}
let request = NSMutableURLRequest(url: URL(string: "http://www.apple.com")!)
observer.addObservable(request)

request.setValue("string", forHTTPHeaderField: "custom")
request.observableDidChange() // notify

ところが、NSMutableString, NSMutableArray, NSMutableDictionary などの一部のオブジェクトは Mutable と謳っていても、実際に変更を加えると、同一オブジェクトのはずなのに通知が行われなくなります。これは クラスタークラスが原因ではないかと考えていますが、今後の課題となりますので、気をつけてください。

extension NSMutableString: ZObservable {}
extension NSMutableArray: ZObservable {}
extension NSMutableDictionary: ZObservable {}

let string = NSMutableString(string: "abc")
let array = NSMutableArray(array: [1, 2, 3])
let dictionary = NSMutableDictionary(dictionary: ["a": 1, "b": 2])

let observer = Observer()
observer.addObservable(string)
observer.addObservable(array)
observer.addObservable(dictionary)

string.observableDidChange() // notify
array.observableDidChange() // notify
dictionary.observableDidChange() // notify

string.append("def")
array.add(4)
dictionary["c"] = 3

string.observableDidChange() // not notify
array.observableDidChange() // not notify
dictionary.observableDidChange() // not notify

その他の方法

その他の方法も考慮される場合は以下のようなリンクが参考になるかもしれません。

環境に関する表記

Xcode Version 8.1 (8B62)
Apple Swift version 3.0.1 (swiftlang-800.0.58.6 clang-800.0.42.1)
8
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5