KVOを使う場合に「よく分からないけれど、とりあえずcontextにはnilを渡しておけばいいいんでしょ!?」と考えて機械的にnil設定している人は実は意外に多いのではないでしょうか?実際、多くの場合、contextは使わなくても問題ありません。しかし、キー値監視するスーパークラスにおいても同じkey/valueを監視しているようなケースにおいてはcontext情報を使ってイベントを識別する必要があります。
下記は監視対象モデルクラスがPerson、「contextを設定して」キー値監視するクラスがViewControllerである場合の例です。後半部分でコード中の各注釈に関して補足説明します。
サンプルコード
環境
- Xcode 7.1.1
- Swift 2.1
- iOS 9.1
import Foundation
class Person: NSObject {
private(set) dynamic var name: String
dynamic var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
import UIKit
// (1) プライベートなグローバル変数を用意
private var kViewControllerContext: UInt8 = 0
class ViewController: UIViewController {
@IBOutlet weak var age: UITextField!
lazy var jonathan = Person(name: "Jonathan", age: 20)
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// (2) コンテキストとしてアドレスを渡す
let opts: NSKeyValueObservingOptions = [.New, .Old]
jonathan.addObserver(self, forKeyPath: "age", options: opts, context: &kViewControllerContext)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
jonathan.removeObserver(self, forKeyPath: "age", context: &kViewControllerContext)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
// (3-1) アドレスを使って自身へのイベントであるか識別する
if context == &kViewControllerContext {
print("self: \(self), keypath: \(keyPath), object: \(object), change: \(change), context; \(context)")
// (4) おまけ
let pointer = UnsafeMutablePointer<UInt8>(context)
let value = pointer.memory
print("pointer: \(pointer)")
print("value: \(value)")
// (3-2) 自身以外のイベントについてはスーパークラスへ
} else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}
@IBAction func changeValue(sender: AnyObject) {
if let number = Int(self.age.text!) where number >= 0 {
print("number: \(number)")
self.jonathan.age = number
}
}
}
補足説明
サンプルコードにおいて特に重要なのは(1)と(3-2)です。
(1) プライベートなグローバル変数を用意
context識別にはアドレスを使います。そのアドレスが割り当てられるプライベートなグローバル変数を用意します。通常、SwiftではUIntよりもIntを使用することが推奨されていますが、今回のケースではアドレスを使うことが目的であるため、サイズの明確化及び無駄なサイズを使わないようにするためにUInt8を使っています。
(2) コンテキストとしてアドレスを渡す
(1)において用意した変数のアドレスをコンテキストとして渡します。
(3-1) アドレスを使って自身へのイベントであるか識別する
自身に対するイベントであるかを識別した上で、必要な処理を実装します。
(3-2) 自身以外のイベントについてはスーパークラスへ
自身に対するイベントでない場合は、スーパークラスに対するイベントである可能性があるため、スーパークラスのメソッドを呼び出します。
(4) おまけ
context識別にアドレスを利用するのがポイントであるため、その中身である値自体にはあまり意味がありませんが、下記のようにして値を取り出すことができます。
let pointer = UnsafeMutablePointer<UInt8>(context)
let value = pointer.memory
print("pointer: \(pointer)")
print("value: \(value)")
最後に
どんな場合においてもcontextを設定する必要があるわけではありません。設定する必要がないことが明確な場合は、contextにはnilを渡すだけで十分だと思います。
[参考URL]
Adopting Cocoa Design Patterns