Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best practices for context parameter in addObserver (KVO)

I was wondering what you should set the Context pointer in KVO when you are observing a property. I'm just starting to use KVO and I haven't gleaned too much from the documentation. I see on this page: http://www.jakeri.net/2009/12/custom-callout-bubble-in-mkmapview-final-solution/ the author does this:

[annView addObserver:self
forKeyPath:@"selected"
options:NSKeyValueObservingOptionNew
context:GMAP_ANNOTATION_SELECTED];

And then in the callback, does this:

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context{

NSString *action = (NSString*)context;


if([action isEqualToString:GMAP_ANNOTATION_SELECTED]){

I'm assuming in this scenario, the author just creates a string to be identified later in the callback.

Then in iOS 5 Pushing the Limits book, I see he does this:

[self.target addObserf:self forKeyPath:self.property options:0 context:(__bridge void *)self];

callback:

if ((__bridge id)context == self) {
}
else {
   [super observeValueForKeyPath .......];
}

I was wondering if there is a standard or best practices to pass into the context pointer?

like image 981
Crystal Avatar asked Oct 04 '12 03:10

Crystal


3 Answers

The important thing is (generally speaking) that you use something (as opposed to nothing) and that whatever you use be unique and private to your use of it.

The primary pitfall here happens when you have an observation in one of your classes, and then someone subclasses your class, and they add another observation of the same observed object and the same keyPath. If your original observeValueForKeyPath:... implementation only checked keyPath, or the observed object, or even both, that might not be sufficient to know that it's your observation being called back. Using a context whose value is unique and private to you allows you to be much more certain that a given call to observeValueForKeyPath:... is the call you're expecting it to be.

This would matter if, for instance, you registered only for didChange notifications, but a subclass registers for the same object and keyPath with the NSKeyValueObservingOptionPrior option. If you weren't filtering calls to observeValueForKeyPath:... using a context (or checking the change dictionary), your handler would execute multiple times, when you only expected it to execute once. It's not hard to imagine how this might cause problems.

The pattern I use is:

static void * const MyClassKVOContext = (void*)&MyClassKVOContext;

This pointer will point to its own location, and that location is unique (no other static or global variable can have this address, nor can any heap or stack allocated object ever have this address -- it's a pretty strong, although admittedly not absolute, guarantee), thanks to the linker. The const makes it so that the compiler will warn us if we ever try to write code that would change the value of the pointer, and lastly, static makes it private to this file, so no one outside this file can obtain a reference to it (again, making it more likely to avoid collisions).

One pattern I would specifically caution against using is one that appeared in the question:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {

    NSString *action = (NSString*)context;
    if([action isEqualToString:GMAP_ANNOTATION_SELECTED]) {

context is declared to be a void*, meaning that that's all the guarantee that can be made about what it is. By casting it to an NSString* you're opening a big box of potential badness. If someone else happens to have a registration that doesn't use an NSString* for the context parameter, this approach will crash when you pass the non-object value to isEqualToString:. Pointer equality (or alternatively intptr_t or uintptr_t equality) are the only safe checks that can be used with a context value.

Using self as a context is a common approach. It's better than nothing, but has much weaker uniquing and privacy, since other objects (not to mention subclasses) have access to the value of self and might use it as a context (causing ambiguity), unlike with the approach I suggested above.

Also remember, it's not just subclasses that might cause pitfalls here; Although it's arguably a rare pattern, there's nothing that preventing another object from registering your object for new KVO observations.

For improved readability, you could also wrap this up in a preprocessor macro like:

#define MyKVOContext(A) static void * const A = (void*)&A;
like image 134
ipmcc Avatar answered Nov 13 '22 23:11

ipmcc


The KVO context should be a pointer to a static variable, as this gist demonstrates. Typically, I find myself doing the following:

Near the top of my file ClassName.m I'll have the line

static char ClassNameKVOContext = 0;

When I begin observing the aspect property on targetObject (an instance of TargetClass) I'll have

[targetObject addObserver:self
               forKeyPath:PFXKeyTargetClassAspect
                  options://...
                  context:&ClassNameKVOContext];

where PFXKeyTargetClassAspect is an NSString * defined in TargetClass.m to be equal to @"aspect" and declared extern in TargetClass.h. (Of course PFX is just a placeholder for the prefix you're using in your project.) This gives me the advantage of autocomplete and protects me from typos.

When I finish observing aspect on targetObject I'll have

[targetObject removeObserver:self
                  forKeyPath:PFXKeyTargetClassAspect
                     context:&ClassNameKVOContext];

In order to avoid too much indentation in my implementation of -observeValueForKeyPath:ofObject:change:context:, I like to write

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (context != &ClassNameKVOContext) {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        return;
    }

    if ([object isEqual:targetObject]) {
        if ([keyPath isEqualToString:PFXKeyTargetClassAspect]) {
            //targetObject has changed the value for the key @"aspect".
            //do something about it
        }
    }
}
like image 19
Nate Chandler Avatar answered Nov 14 '22 01:11

Nate Chandler


I think the better way would be to implement it as apple's doc says:

The address of a uniquely named static variable within your class makes a good context.

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;

see documentation.

like image 4
Mykhailo Lysenko Avatar answered Nov 14 '22 00:11

Mykhailo Lysenko