Key-Value Observing for the mainstream
I really enjoy reading the NSHipster blog. The article about Key-Value Observing (KVO) from October 7th was however slightly misleading and should be taken with a grain of salt.
I wasn’t expecting much after the introduction (“Key-Value Observing has the worst API in all of Cocoa”) but unfortunately it went from bad to worse.
I don’t like to point fingers without explaining what I think is wrong and what I would have done instead so I will try to expose my thoughts in this post.
NSKeyValueObservingOptionInitial is indeed compelling
When discussing the method for adding an observer, Mattt explains:
Yuck. What makes this API so unsightly is the fact that those last two parameters are almost always 0 and NULL, respectively.
I will discuss the context in the next section but for now let’s see what are the benefits of the NSKeyValueObservingOptions
, in particular NSKeyValueObservingOptionInitial
.
How many times have you written code similar to the following:
- (void)loadView
{
[super loadView];
[_object addObserver:self forKeyPath:@"someKey" options:0 context:NULL];
[self _updateSomething];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
[self _updateSomething];
}
What if I told you that you can get rid of that first _updateSomething
call? That’s right, by using NSKeyValueObservingOptionInitial
your observer will be notified (and the _updateSomething
called) even before the registration completes. Your update code is now concentrated in the observation handling and you don’t need to duplicate code or even have to worry about making sure that method was called when setting up the object.
It might sound like very little but trust me it’s actually quite nice in practice.
Always, always, always use the context
Granted, Mattt does talk about using the context but unfortunately, the way it’s discussed implies that it’s an edge case you shouldn’t really worry or care about.
As for context, this parameter is a value that can be used later to differentiate between observations of different objects with the same key path. It's a niche use case, ...
The truth is, in practice it is very important to always use the context to differentiate between observed objects and not rely on the key path. We are working with an object-oriented language and we should always accommodate for the case where a class might be subclassed or a super class expecting a behavior that your own class will prevent.
Let’s take an example. Assume you have the following class hierarchy (see this gist for the complete example):
#import <Foundation/Foundation.h>
@interface Stalked : NSObject
@property (assign, nonatomic) NSInteger tag;
@end
@implementation Stalked
@end
@interface Parent : NSObject
{
@protected
Stalked *_stalked;
}
@property (strong, nonatomic) Stalked *stalked;
@end
@implementation Parent
- (id)init
{
self = [super init];
if (self == nil) {
return nil;
}
_stalked = [[Stalked alloc] init];
[_stalked addObserver:self forKeyPath:@"tag" options:0 context:NULL];
return self;
}
- (void)dealloc
{
[_stalked removeObserver:self forKeyPath:@"tag" context:&_ParentStalkedTagContext];
}
@end
@interface Child : Parent
@end
@implementation Child
- (id)init
{
self = [super init];
if (self == nil) {
return nil;
}
[_stalked addObserver:self forKeyPath:@"tag" options:0 context:NULL];
return self;
}
- (void)dealloc
{
[_stalked removeObserver:self forKeyPath:@"tag" context:&_ChildStalkedTagContext];
}
@end
int main(int argc, const char **argv)
{
Child *child = [[Child alloc] init];
[[child stalked] setTag:2];
return 0;
}
As you can see, we have a simple class hierarchy: Child
subclassing Parent
itself subclassing NSObject
. Parent
has a stalked
property. Both the Parent
and Child
are observing the tag
key path on the Stalked
object. Simple and pretty common scenario. You can notice that neither the child nor the parent are specifying a context when registering as an observer.
Now if we had to implement the observation as described in the article, what do you think would happen?
// In Parent
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:@"tag"]) {
NSLog(@"Parent stuff");
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
// In Child
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:@"tag"]) {
NSLog(@"Child stuff");
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
That’s right, since the Child
overrides this method it would be called twice: once for the child and once for the parent. However, since it’s only filtering based on the key path this will print Child stuff twice and the parent will never have a chance to see the changes. This is bad.
What you should do instead is use the context as a way to filter between observers.
// In Parent
static NSString * _ParentStalkedTagContext = @"_ParentStalkedTagContext";
[_stalked addObserver:self forKeyPath:@"tag" options:0 context:&_ParentStalkedTagContext];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == &_ParentStalkedTagContext) {
NSLog(@"Parent stuff");
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
// In child
static NSString * _ChildStalkedTagContext = @"_ChildStalkedTagContext";
[_stalked addObserver:self forKeyPath:@"tag" options:0 context:&_ChildStalkedTagContext];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == &_ChildStalkedTagContext) {
NSLog(@"Child stuff");
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
By doing such, the child observation method will still be called twice but this time the second call will be filtered out and dispatched to the parent instead.
I cannot believe anyone slightly experienced with KVO wouldn’t see the benefit of doing this.
Unsubscribing is no madness!
In KVO, subscribing and unsubscribing follows a very simple rule: you are responsible for unsubscribing to things you subscribed to.
There is no need to check whether an object is observing another object to decide whether it should unsubscribe. An object should be able to track whether it subscribed to some changes on the key path of another object in the first place and unsubscribe from it in due time.
Does this remind you of something? To me it sounds exactly like reference counting in memory management. Would you ever write the following code?
while ([object retainCount] > 0) {
[object release];
}
I don’t think so.
Or, assuming every object had a table of objects that called retain
on it, would you write the following code?
if ([[object retainers] containsObject:self]) {
[object release];
}
Sounds unlikely.
Or even worse, would you ever consider handling a signal for a memory error due to calling free
on a freed piece of memory because you couldn’t remember whether you previously retained this object and decided to release it anyway.
No. Because this is madness.
So why would you try and catch an exception when attempting to remove an observer for something you weren’t observing in the first place?
Remember the simple rule: remove an observer only if you were observing it in the first place.
keyPathsForValuesAffectingValueForKey:
Always call super in keyPathsForValuesAffectingValueForKey:
is an amazing feature of KVO that allows one to set a dependency tree for values affecting a key path.
Imaging having a title
property in a parent view controller depending on the title in each child view controller, itself depending on the title in its own children. Using this method you can have the parent view controller’s title depend on the whole tree of titles without having to write any change notification code or having to update state for each object on the tree: how declarative!
Mattt’s example is as following:
+ (NSSet *)keyPathsForValuesAffectingAddress
{
return [NSSet setWithObjects:@"streetAddress", @"locality", @"region", @"postalCode", nil];
}
I have a problem with this: it doesn’t take into consideration what super
might have to say about this key path (assuming the superclass actually itself responds to address
).
The right way, in my opinion, to write this is as following:
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
NSMutableSet *keyPaths = [NSMutableSet setWithSet:[super keyPathsForValuesAffectingValueForKey:key]];
if ([key isEqualToString:@"address") {
[keyPaths addObjectsFromArray:@[@"streetAddress", @"locality", @"region", @"postalCode"]];
}
return keyPaths;
}
If the superclass returns some key paths affecting this key, they will be taken into considerations.
Conclusion
Sorry about the rant. I’d like to reiterate that I have a great esteem of the NSHipster blog. I read amazing articles and learned a ton of things from it. I guess that’s why it feels weird to read misleading information once in a while ;)
Keep on the good work Mattt.