A moderate improvement to Cocoa’s Key-Value Observing
The background
Key-Value Observing (KVO) allows Objective-C instances to track changes in the properties of other objects. It is a powerful programming tool and is implemented in an interesting manner. But the API to use it is lamentable — Apple’s interfaces usually stand out as shining examples of object-oriented design; the interface for registering and unregistering a KVO observer feels like an accident that can’t even be fully documented.
It is something of a rite-of-passage for Cocoa programmers to to workaround the weaknesses they see in Apple’s provided KVO interface. The inability to unregister an observation without upsetting a potential subclass had bothered me, but only in a theoretical sense, so I mostly ignored it. When I started writing a Garbage Collected app, I realized the time for my workaround had come, and I finally wrote NSObject+TLKVO.
The foreground
As implied by the name, NSObject+TLKVO is a category on NSObject. It replaces Cocoa’s built-in KVO -addObserver and -removeObserver methods with ones that solve two problems:
- Each class can safely start and stop observing a key path without disturbing its subclasses
- To run under Garbage Collection, a class does not need to implement some rubbish extra reference counting to safely remove itself from observed objects
As a bonus, it’s not even necessary to remove TLKVO-registered observers in garbage collected code at all. Memory-managed code can conveniently unregister all observations for a given class with one line of code as well.
I keep mentioning how a “class” registers for an observation. This is the key to the TLKVO design: it doesn’t change the overall KVO mechanism, it just refines it by making registration/unregistration for a key path unique to an instance AND a Class, rather than just an instance.
The code
Using it can be very simple:
TLKVORegisterSelf(watchedObject, @"watchedPath.watchedKey", NSKeyValueObservingOptionNew);
TLKVORegisterSelf(watchedObject, @"anotherWatchedKey", NSKeyValueObservingOptionNew);
TLKVOUnregisterSelf(watchedObject, nil);
or:
[watchedObject tl_addObserver:self ofClass:[MyClass class]
forKeyPath:@"watchedPath.watchedKey" options: NSKeyValueObservingOptionNew]
[watchedObject tl_addObserver:self ofClass:[MyClass class]
forKeyPath:@"anotherWatchedKey" options: NSKeyValueObservingOptionNew]
[watchedObject tl_removeObserver:self ofClass:[MyClass class] forKeyPath:nil];
It’s important to note that in second (non-convenience) example, I used [MyClass class]
instead of [self class]
. This distinction is key. At runtime, [self class]
will return the class of the instance, which may be a subclass. Instead, TLKVO needs to know the class which you are currently implementing so it can properly unique the registration and properly target the change observation messages.
Observing the changes is basically, well, unchanged:
- (void)observeValueForKeyPath:(NSString*)keyPath
ofObject:(id)object
change:(NSDictionary*)change
context:(void*)context
{
if (context == &TLKVOContext) {
NSLog(@"Observed %@ of %@", keyPath, object);
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
The only difference to what you would write with plain-old KVO is the use of &TLKVOContext
instead of your own unique pointer. If your class’s method is asked to observe a change with a context of &TLKVOContext
, you can be sure that it is due to an observer registration made by the same class.
The what it doesn’t do
If you thought the context pointer could be useful for passing some extra information to your change observing method, you might be disappointed by not being able to define it yourself. This omission is intentional. Due to the design of Cocoa’s KVO, you can’t do any introspection of the pointer until you are certain that it belongs to your class. If you want to use an NSDictionary instance as this context, you need to keep its pointer accessible to the change observing method anyway.
You might argue that a single class may want to divide itself internally into separate sections and use different unique pointers to help sort out changes observed by each section. In that case, you probably also need more fine-grained registration and unregistration. Your -observeValueForKeyPath method is probably getting pretty complicated, too. At this point, you might want to check out…
The further reading
Another potential shortcoming of Cocoa’s KVO architecture is that all observer notifications get delivered to a single method. This can be convenient if all the changes are related, but sometimes this method becomes a readability bottleneck instead.
Several other developers have shared their improvements to KVO:
- MAKVONotificationCenter
- Mike Ash explains the problems with KVO much more clearly than I have, and provides an NSNotificationCenter-style replacement. Note that this was written before Garbage Collection, and read the discussion in the comments as well.
- KVOBlockNotificationCenter
-
Jonathan “schwa” Wight has extended Mike’s idea and made it work better with Garbage Collection and blocks.
- KVO+Blocks
- Andy Matuschak presented a clean, simplified API for having a block fire for every change before 10.6 was released. I’m not sure if he’s released the code yet.
If you just want more information on using KVO as Cocoa provides it, check out Dave Dribin’s article on Proper KVO usage or this Apple framework engineer’s recommendations on the matter.
I may update this post if more KVO extensions or further reading come to my attention.