Key Value Observing Improvements
Key value observing is quite a useful tool, no doubt about it. But it has a singularly annoying manner of informing the observer of a change. The -[NSObject observeValueForKeyPath:ofObject:change:context:]
method is sent to the observer when a change occurs. It's up to the implementor to parse the `change' dictionary to figure out what changed.
When I use observers, I usually want a method called on my class when a change occurs, similar to target/action with UI elements. So my -[NSObject observeValueForKeyPath:ofObject:change:context:]
is basically a set of if's for each key I'm observing that calls the appropriate method.
This turns out to be pretty nasty, especially when you have multiple classes in a hierarchy implementing the method. It wasn't obvious to me that I needed to call super in this method. Thirty minutes of debugging later, and I was convinced there had to be a better way of doing it.
To this effort, I present two new APIs. To NSObject, I add -[NSObject addObserver:forKeyPath:options:selector:]
and to NSArray, -[NSArray addObserver:toObjectsAtIndexes:forKeyPath:options:selector]
. The code is available here, or keep reading for the details.
Update 1.1: Two critical bug fixes. Please update to the new version if you have the old one. Also, anonymous svn is now available if you want to use an external. http://svn.shiftedbits.org/public/KVOAdditions/trunk.
NSObject:
@interface NSObject (KVOAdditions) - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options selector:(SEL)aSelector; @end
NSArray:
@interface NSArray (KVOAdditions) - (void)addObserver:(NSObject *)observer toObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options selector:(SEL)aSelector; @end
These allow you to observe a keyPath on an object and receive a message to the designated selector when the change occurs. Furthermore, the selector can have four different signatures that allow for different levels of information to be received. These selector formats are as follows:
/* Receive no information about the change */ - (void)valueDidChange; /* Receive the raw change dictionary */ - (void)valueDidChange:(NSDictionary *)change; /* If the observing options include either * NSKeyValueObservingOptionOld or NSKeyValueObservingOptionNew, * receive the old and new values */ - (void)valueDidChange:(id)old newValue:(id)new; /* If the observing options include either * NSKeyValueObservingOptionOld or NSKeyValueObservingOptionNew, * receive the old and new values */ - (void)valueDidChange:(id)old newValue:(id)new prior:(BOOL)isPrior;
Feedback on these APIs, including suggestions and criticism (and even bugs!) are of course welcome.
4 comments:
Steve at 2008-07-24 11:45:04 -0400
I have written code that uses the key and creates a selector "keyChanged" and if its implemented, calls that. If that is not implemented, it has other fallbacks to try. You COULD make this a category on NSObject and never write another observeValueForKeyPath method. You could also see if the context argument is a string and use it help your code decide what to do. Also, keep in mind that the context argument is NOT optional and MUST always be provided and if it is not what you expect, then call [super observeValueForKeyPath....]
Devin Lane at 2008-07-24 13:15:48 -0400
Steve: This is already a category on NSObject. I'm not sure what you mean. How would you determine if the context argument is a string? It's not as though you can call methods on it. The context argument is most certainly optional, in the sense that anything that can be interpreted as a pointer (an integer of the right size basically) can be passed. I pass NULL almost all the time. However, setting the context argument to self could be useful for determining if a call to super is needed.
Christiaan at 2008-08-19 11:25:20 -0400
The context argument is NOT optional in practice, the fact that the API allows it is no reason you should pass NULL. You should ALWAYS use a context, and I'm quoting several well known Apple engineers. You can google for detailed reasons. The main reason is that otherwise you can NEVER know if the observation message is meant for you (there can be any number of observers for the same key/object pair). Moreover, it should always be a unique pointer, and 'self' is far from unique (given the context). A simple solution is to use a static NSString * with a unique name.
Devin Lane at 2008-09-03 00:42:43 -0400
Christian: You raise some good points about the context argument. I've released an update (see http://shiftedbits.org/2008/09/03/key-value-observing-improvements-v12/) that includes the selector in determining the uniqueness of an observation. This allows a class and a subclass to observe a single property on a single object with different selectors. This has several advantages over the static NSString* method: 1. No need to declare said strings. 2. No need to check in your -observeValueForKeyPath method if the context is your context. 3. No need to call super.