Cocoaフレームワークでしばしば登場するキー値監視。Key Value Observing や KVO とも。
「あるインスタンスのプロパティが更新されたときに、何かしらの処理をしたい」
そういうシーンで利用する。
つまり、Observerパターン。
まずはモデルを定義。
nameプロパティを持つ単純なクラス。
@interface Model : NSObject
@property (nonatomic, assign) NSString *name;
@end
@implementation Model
@synthesize name;
@end
続いて、プロパティが変更されたときに行いたい処理を書く。
@interface Kanshi : NSObject
@end
@implementation Kanshi
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if([keyPath isEqualToString:@"name"]) {
NSLog(@"%@", [object valueForKey:@"name"]); // nameプロパティの値が取れる
}
}
@end
-(void)observeValueForKeyPath: ofObject: change: context:
という長い名前のメソッドをオーバーライドしているのがコツ。
この2つのクラスを使ってテストしてみる。
int main()
{
@autoreleasepool {
Model *model = [[Model alloc] init];
Kanshi *kanshi = [[Kanshi alloc] init];
[model addObserver:kanshi forKeyPath:@"name" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:callback];
[model setValue:@"hoge" forKey:@"name"];
model.name = @"hoge2";
[model removeObserver:kanshi forKeyPath:@"name"];
[model setValue:@"hoge3" forKey:@"name"];
[kanshi release];
[model release];
}
return 0;
}
まず最初に -(void)addObserver: forKeyPath options: context: を使って監視対象のインスタンスに、更新があった際に通知してほしいインスタンスを登録する。
いくつでも登録することができる。
forKeyPath には監視したいプロパティ名を、options は後述する。
context は(void *)なので何でも好きなポインタを渡せば良い。
続いて、敢えて二通りの方法でプロパティに値をセットしている。
キー値コーディングと普通のセッターで値をセットしている。
どちらの方法でも通知されるということを示したかっただけ。
つまりこのコードでは、2回プロパティが更新されたことになるので、当然通知は2回される。
さて、通知されると書いたがどういうことか?
先ほど、-(void)observeValueForKeyPath: ofObject: change: context: という長いメソッドをオーバーライドした。
いよいよ出番だ。
監視対象のプロパティが更新されるたびに上記の長いメソッドが呼ばれる。
keyPath にはプロパティ名が、object には監視対象となっているインスタンスが、change には変更の状態が、context には任意のポインタが入っている。
change は -(void)addObserver: forKeyPath options: context: のoptionsに渡した値によって登録されている内容が異なる。
change の中身。
{
kind = 1; <- 常に入っている。意味はよくわからん。
new = hoge; <- NSKeyValueObservingOptionNewを指定したときに入っている(今の値が)
old = "<null>"; <- NSKeyValueObservingOptionOldを指定したときに入っている(1つ前の値が)
}
特に用途がなければ options:0 で良いでしょう。
context は addObserver時の context に渡したポインタが入っている。
関数ポインタを渡してコールバック関数を呼び出しても良いし、せっかくの Objective-C なのでセレクタ(SEL型)を渡しても良いし、他のクラスのインスタンスを渡しても良い。
ご自由に。
さて、では後始末について。
監視対象のインスタンスの -(void)removeObserver: forKeyPath: を呼んで削除する。
第一引数には通知先として登録したインスタンス、第二引数には監視対象としていたプロパティ名を。
ちなみに -(void)addObserver: forKeyPath options: context: で通知先として登録したインスタンスは保持されない。
[kanshi retainCount] の値が変わらないことはすぐに観察できる。
最後にもう一回、name プロパティを更新しているが通知は行われない。