MVCでImageを検索してみると、想像している以上にいろいろな、MVCの図に遭遇します。どの図がいいとかの話は置いておくとして、Model に変更があった場合、何らかの方法で View または ViewController はその変更を検知して表示に反映させたいものです。
今回は、このモデルの変更の通知を簡単にする為に、軽量のユーティリティコードを開発したので紹介したいと思います。
ZObservable
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)