Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's wrong with this observeValueForKeyPath:ofObject:change:context: implementation?

In my UIScrollView subclass, I'm observing frame changes:

[self addObserver:self forKeyPath:@"frame" options:0 context:NULL];

My observeValueForKeyPath:ofObject:change:context: implementation is as follows:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (object == self && [keyPath isEqualToString:@"frame"]) {
        [self adjustSizeAndScale];
    }
    if ([UIScrollView instancesRespondToSelector:@selector(observeValueForKeyPath:ofObject:change:context:)]) {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; // Exception
    }
}

But I get exception with this code:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '<WLImageScrollView: 0x733a440; baseClass = UIScrollView; frame = (0 0; 320 416); clipsToBounds = YES; layer = <CALayer: 0x7346500>; contentOffset: {0, 0}>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
Key path: frame
Observed object: <WLImageScrollView: 0x733a440; baseClass = UIScrollView; frame = (0 0; 320 416); clipsToBounds = YES; layer = <CALayer: 0x7346500>; contentOffset: {0, 0}>
Change: {
    kind = 1;
}
Context: 0x0'

Does it mean UIScrollView implements observeValueForKeyPath:ofObject:change:context: but throws the above exception?

If so, how can I properly implement observeValueForKeyPath:ofObject:change:context: so that I can both handle my interested changes and give superclass a chance to handle its interested changes?

like image 200
an0 Avatar asked Jul 04 '11 17:07

an0


2 Answers

You should add a context value when you add the observer. In your -observeValueForKeyPath method, check the context parameter. If it is not the context you passed when you added the observer, then you know this message is not intended for your subclass, and you can safely pass it on to the superclass. If it is the same value, then you know it's intended for you, and you should not pass it on to super.

Like this:

static void *myContextPointer;

- (void)addSomeObserver {
    [self addObserver:self forKeyPath:@"frame" options:0 context:&myContextPointer];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context != &myContextPointer) {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
    else {
        // This message is for me, do whatever I want with it.
    }
}                                  
like image 184
BJ Homer Avatar answered Nov 10 '22 15:11

BJ Homer


Edit: BJ Homer's answer is probably a better approach to take here; I forgot all about the context parameter!

Even though calling the super implementation is by-the-book, it seems like calling observeValueForKeyPath:ofObject:change:context: on UIKit classes that don't actually observe the fields in question throws an NSInternalInconsistency exception (not the NSInvalidArgumentException you would get with an unrecognized selector). The key string in the exception that suggests this to me is "received but not handled".

As far as I know, there's no well-documented way to find out if an object observes another object on a given key path. There may be partially-documented ways such as the -observationInfo property which is said to carry information on the observers of an object, but you're on your own there—it's a void *.

So as I see it, you've got two options: either don't call the super implementation or use an @try/@catch/@finally block to ignore that specific type of NSInternalInconsistencyException. The second option is probably more future-proof, but I have a hunch that some detective work could get you more satisfying results via the first option.

like image 29
Joe Osborn Avatar answered Nov 10 '22 15:11

Joe Osborn