What's wrong with this observeValueForKeyPath:ofObject:change:context: implementation?
Solution 1
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.
Solution 2
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.
}
}
![an0](https://i.stack.imgur.com/UnuCZ.jpg?s=256&g=1)
an0
Updated on June 29, 2022Comments
-
an0 almost 2 years
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? -
an0 almost 13 yearsThe article you linked to is talking about another thing: the superclass does NOT implement observeValueForKeyPath:ofObject:change:context:. But here as you can see from the code, UIScrollView does implement it, otherwise super method will not be called.
-
Joe Osborn almost 13 yearsEven if it does implement the method, it might have code that detects unobserved keys and throws exceptions for them—thus the
NSInternalInconsistency
exception "received but not handled", which is different from theNSInvalidArgumentException
"unrecognized selector sent to instance". -
an0 almost 13 yearsThat's what I'm asking. So the default implementation of observeValueForKeyPath:ofObject:change:context: in NSObject throws exception? If so, there is nothing we can do to detect whether superclass wants to be notified of key value changes, isn't there?
-
Joe Osborn almost 13 yearsThe implementation in some UIKit classes, at least, certainly seems to. My guess, then, is "no, there's no (safe) way to detect it". The safest way is probably to look for that exception in an
@try
. Off-the-record, I would say that you might try looking at the-observationInfo
property of the object you're interested in (self
) on the off chance that it might help you find out what keys the parent class is observing. -
an0 almost 13 yearsThanks:) Do you mind improving your answer with your comments so I can accept it?
-
Tom Abraham almost 12 yearsWhy can't you just handle the keypaths for the objects for which you want to and let the UIScrollView handle the rest?
-
Joe Osborn almost 12 yearsBecause if you pass along an object/key path pair that
UIScrollView
doesn't know to observe, it won't just ignore it--it'll throw an exception. Note that in the original question, the asker wants aUIScrollView
subclass to observe itself to catch certainframe
changes, since there's no available API besides KVO for that. Given that there's no documented way to know for sure if an object is observing a given object/key path pair, one cannot determine which pairs will and won't throw, so the most correct answer is probably to pass everything and catch any exceptions that arise. -
BJ Homer almost 12 yearsIt's actually not hard to know if the message is meant for you... just look use the 'context' parameter.
-
Joe Osborn almost 12 yearsYeah, that's a good idea. You've still got to pass along observations without your special context up to the superclass, and there may be pathological cases where that is unsafe to do in general, but that's a good starting point for sure.
-
Nate Chandler almost 12 yearsIt would be better to do something like
static int myContext;
and then[self addObserver:self forKeyPath:@"frame" options:0 context:&myContext];
. This way, the address pointed to is guaranteed to be unique. -
BJ Homer almost 12 yearsOh, you're right, it should absolutely be
&myContextPointer
, not justmyContextPointer
. But I don't see why anint
would be any better than avoid *
. -
Nate Chandler almost 12 yearsNothing better about using an int, I don't think. It's just slightly shorter to type
static int myContext;
than to typestatic void *myContext;
. (-: I guess you could say, also, that it let's the compiler remind you that you should be using&myContext
rather than justmyContext
--ifmyContext
's type isint
the compiler will warn that you're doing an incompatible integer to pointer conversion. -
BJ Homer almost 12 yearsHonestly, I usually do
static void *myContext = &myContext;
, which is nice because thenmyContext == &myContext
, and you can use either one. But I didn't feel like trying to explain that little mess in an answer mostly unrelated to the topic. -
Mark over 4 years@BJHomer nice, I've never come across this
static void *myContext = &myContext;
thing but now it seems so obvious. Thanks!