Key-value observing (KVO) はオブジェクトのプロパティが変更されたときに自動で通知するような仕組みです。
この記事ではKVOを使う上で便利になる機能や注意点などを紹介します。
KVOの基礎
まず、KVOについて簡単に説明します。
Appleの公式日本語ドキュメントにKVOのドキュメントもありますので、詳細はそちらを参照してください。
監視オブジェクトの追加
あるオブジェクトがあるとします。
@interface SomeObject : NSObject @property(nonatomic,assign) NSUInteger value; @end
このオブジェクトのインスタンスからプロパティvalue
の値を監視できるようにするには次のようにします。
self.someObject = [[SomeObject alloc] init]; [self.someObject addObserver:self forKeyPath:@"value" options:0 context:NULL];
これで変更があったことをselfが受けられるようになります。
監視メッセージの受信
先ほどのオブジェクトの値変更の通知を受けるメソッドはobserveValueForKeyPath:ofObject:change:context:
で、ここに処理を書いていきます。
次の例ではとりあえずログにメッセージを表示させるようにしています。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSLog(@"keyPath=%@\nobject=%@\nchange=%@\ncontext=%@", keyPath, object, change, context); }
実際に値をインクリメントしてみましょう。
self.someObject.value++;
この結果、ログには次のように出力されるでしょう。
KVOSample[25664:c07] keyPath=value object=<SomeObject: 0x93497f0> change={ kind = 1; } context=(null)
Tip 1: オプションで通知内容をカスタマイズする
先ほどのログで{kind = 1}
というのは、この通知による変更がどのようなものかを示すもので、これは、
@{ NSKeyValueChangeKindKey: @(NSKeyValueChangeSetting) }
と等価であり、すなわち値の変更を意味しています。
to-oneな関係の場合にはNSKeyValueChangeSetting
しか返ってきませんが、配列などのto-manyな関係では要素の追加を意味するNSKeyValueChangeInsertion
などが返されることがあります。
また、addObserver:forKeyPath:options:context:
でオプションの値を指定することで、このchange
ディクショナリに情報を付加することができます。
例えば、次のようにすると、observeValueForKeyPath:ofObject:change:context:
のchange
にvalueの変更前と後の値が付加されます。
[self.someObject addObserver:self forKeyPath:@"value" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:NULL];
このときにインクリメントさせた場合の出力結果は次のようになり、値が 0 から 1 に変更されたことがわかるようになります。
KVOSample[56556:c07] keyPath=value object=<SomeObject: 0x93497f0> change={ kind = 1; new = 1; old = 0; } context=(null)
前の値を取るのは意外と面倒だったりしますが、これを使えば簡単に取得することができます。
Tip 2: OCMockでKVOのテストを行う場合の注意
OCMockでKVOの通知が来るかどうかをテストするときには、mockとなるオブジェクトはNSObjectがベースでよいのです。
しかし、そのまま利用すると次のように、isKindOfClass:
がないと言われてしまいます。
Unknown.m:0: error: -[MyKVOTest testNotification] : OCMockObject[NSObject]: unexpected method invoked: isKindOfClass:<??>
これに対処するために、モックでandReturnValue:
を利用してisKindOfClass:
を捏造してやります。結果としてテストコードは次のようになります。
id mock = [OCMockObject mockForClass:[NSObject class]]; [[[mock stub] andReturnValue:OCMOCK_VALUE((BOOL){NO})] isKindOfClass:[OCMArg any]]; [[mock expect] observeValueForKeyPath:@"value" ofObject:self.someObject change:@{NSKeyValueChangeKindKey:@(NSKeyValueChangeSetting)} context:NULL]; [self.someObject addObserver:mock forKeyPath:@"value" options:0 context:NULL]; self.someObject.value++; [mock verify];
参考資料:
- objective c - Mocking KVO with OCMock - Stack Overflow
- objective c - why can I not just pass an int or bool directly into the OCMock’s “andReturnValue” argument? - Stack Overflow
Tip 3: コンテキストでセレクタを指定する
Notificationとは違い、KVOは受けるセレクタは固定なため、大量のKVOを利用するときに大量のif-then処理が必要となります。
そこで、いまいち使い途がなさそうなcontext
をメソッド呼び出しに利用してしまおうという話です。
observeValueForKeyPath:ofObject:change:context:
はただのトランポリンになります。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSMethodSignature* sig = [[self class] instanceMethodSignatureForSelector:context]; NSInvocation* inv = [NSInvocation invocationWithMethodSignature:sig]; [inv setTarget:self]; [inv setSelector:context]; [inv setArgument:&object atIndex:2]; [inv setArgument:&keyPath atIndex:3]; [inv setArgument:&change atIndex:4]; [inv invoke]; }
そして、例えば次のようなメソッドを作成した上で…
- (void)onFooChanged:(id)object keyPath:(NSString*)keyPath change:(NSDictionary*)change { NSLog(@"keyPath=%@\nobject=%@\nchange=%@", keyPath, object, change); }
次のようにコンテキストにセレクタを指定すればよいでしょう。
[self.someObject addObserver:self forKeyPath:@"foo" options:0 context:@selector(onFooChanged:keyPath:change:)];
おわりに
KVOのTipsを紹介しました。これ以外にも、「Objective-Cプログラミングの概念 (pdf)」(Concepts in Objective-C Programming)ではReceptionistパターンの紹介があります。
0 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。