I would normally title this something more clever, but I want this post to be findable by people who are looking for exactly this. Thus, we stick to the convention.
KVO, or key-value observing, is a pattern that Cocoa provides for us for subscribing to changes to the properties of other objects. It’s hands down the most poorly designed API in all of Cocoa, and even when implemented perfectly, it’s still an incredibly dangerous tool to use, reserved only for when no other technique will suffice.
I want to run through a few ways that KVO is really problematic, and how problems with it can be avoided. The first and most obvious problem is that KVO has a truly terrible API. This manifests itself in a few ways:
Let’s say we want to observe the contentSize
of our table view. This seems like a great use for KVO, since there’s no other way to get at the information about this property changing. Let’s get started. To register for updates on an object’s property, we can use this method:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
Seems simple enough, self
is the observer
and the keyPath
is contentSize
. No clue what those other parameters do, so let’s leave them alone for now.
[_tableView addObserver:self forKeyPath:@"contentSize" options:0 context:NULL];
Great. Now let’s respond to this change:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { [self configureView]; }
Awesome. We’re done. Just kidding. We have no control over the signature of this method, and it must handle all of our KVO listeners. We should make sure that we’re only responding to our own KVO notification, so we’re not doing extra work.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (object == _tableView && [keyPath isEqualToString:@"contentSize"]) { [self configureView]; } }
The @"contentSize"
key path is string-ly typed. This means that we’re not providing the compiler or the analyzer with any information about what kind of property it is or whether it even exists, and the compiler can’t know to change it when we run the Refactor > Rename command. It’s just a string. (Cool thing that I learned, Refactor > Rename actually does change strings in the -valueForKey:
method. There’s still no way to get real type information out of the id
return value.)
We have to use NSStringFromSelector(@selector(contentSize))
here, to give the compiler hints about whether this property exists and being able to rename it when the time comes. Of course, we can’t do this for key paths that are multiple values deep, such as if we were observing a view controller and wanted to get the content offset of the scrollview, with the key path: scrollview.contentOffset
. Since there’s nothing we can do about that, it’s fortunate that this simple case doesn’t have that problem.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (object == _tableView && [keyPath isEqualToString:NSStringFromSelector(@selector(contentSize))]) { [self configureView]; } }
Great! Well, not yet. We might have a superclass that also implements this method because it has its own KVO listeners. We should also call super to make sure notifications get to it. If we forget to call super at any point, none of the objects in the superclass chain will receive notifications.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (object == _tableView && [keyPath isEqualToString:@"contentSize"]) { [self configureView]; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } }
You should not pass up any observer notifications that you have handled, or else KVO will complain. Note that the docs suggest calling the superclass’s implementation “if it implements it”, implying that NSObject
doesn’t implement it. Even though it’s not documented, NSObject
does in fact implement it, probably to prevent us from having to worry about this particular problem.
We want to be good citizens, and we want to remove our particular observance on -dealloc
, so we call:
[_tableView removeObserver:self forKeyPath:NSStringFromSelector(@selector(contentSize)) context:NULL];
Now, this isn’t strictly necessary, since the table view will probably get deallocated also, and won’t be able to fire any more notifications. Nevertheless, we should take care of this. An important thing to note is that KVO will throw an exception and crash our app if we try to remove the same observance twice. What happens if a superclass were also observing the same parameter on the same object? It would get removed twice, and the second time would cause a crash.
Hm. Maybe this is where the context
parameter comes in? That would make sense. We could pass a context
like self
, which would make perfect sense, since this object is the context. However, this wouldn’t help us in the superclass case (since self
would point to the same object in the superclass as well as the subclass). Mattt Thompson at NSHipster recommends using a static pointer that stores its own value for the context, like so:
static void *ClassNameTableViewContentSizeContext = &ClassNameTableViewContentSizeContext;
This should be scoped to our file, so that no other files have access to it, and we use a separate one for each key-value observation we do. This helps to make sure that none of our super- or subclasses can deregister us from being an observer.
We can use this context when registering and then also check it in the observation method. Now that we have contexts, we only want to pass the notification along to super if we didn’t use it ourselves. One nice thing about having a pointer that is specific to this one exact observance is that we can check just this parameter, instead of having to check the object
and keypath
parameters, simplifying our code.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == ClassNameTableViewContentSizeContext) { [self doThing]; } else if (context == OtherContext) { [self doOtherThing]; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } }
N.B.: We always call out to a different method instead of trying to do our work inside the method, since the observeValueForKeyPath:
method will get really gnarly really fast.
All of these nuances in the API cause KVO to embody what is known as a pit of failure rather than a pit of success. The pit of success is a concept that Jeff Atwood talks about. APIs should be designed so that they guide you into using them successfully. The should give you hints as to how to use them, even if they don’t explain why you should use them in that particular way.
KVO does none of those things. If you don’t understand the subtleties in the parameters, or if you forget any of the details in implementation (which I had done and only noticed because I went back to my code to reference it while writing this blog post), you can cause horrible unintended behaviors, such as infinite loops, crashes, and ignored KVO notifications.
Even if KVO had a great API and were easy to use, it has other problems as well. Rule #2 of Pep 20 is:
Explicit is better than implicit.
KVO is the implicit way of updating with state changes. Tiny changes in one corner of your app can have ripple effects into other unexpected corners. You have no way of really keeping track of what’s being observed and by whom, and you have no way of using Xcode’s tools, like Command-click, to follow the path of code changes. The only thing you can hope to do is keep all your observations in your head at all times, which falls apart hopelessly as soon your app gets to any meaningful size or you add even one more person to your team.
A reader commented on Twitter with a great phrase that describes the problem exactly: he called KVO “a non-local implicit control flow change”. You’re essentially injecting code into setters at runtime from another object entirely.
When compared with a delegate pattern, KVO shows how weak it is in this area. For example, in most cases, there is only one delegate at any given time, simplifying which objects you need to check when problems arise. Since you have to implement a method with a custom selector, it is very easy to track down which objects in your app can ever even be the delegate for a particular protocol. It also provides casting and type information to its delegates, which reduces your reliance on guessing the type or checking at runtime with isKindOfClass:
the way you have to with KVO.
You also have to make sure that you’re not changing the property that you’re observing when you’re responding to it, because then you’ll get another notification, ad infinitum. You never know if you’re changing another property that will also trigger a separate KVO notification, making debugging all the more difficult.
Of course, if you’re taking a lock when using your setter, your observers will be fired with that lock still held. Every time we think we have a grasp on the nuances of KVO, it hits us with a new problem.
Even if we use queues where we used to use locks, the same problem exists here as well. The KVO observer method will be fired on whatever thread the setter was called on, at which point you accidentally update your UI from the main thread, and your UI enters an inconsistent state, and your QA tester files a completely inexplicable bug. NSNotificationCenter
has the same problem, but at least you’re explicitly publishing a notification in that case. The sender has control over when that notification gets posted, instead of the sender having no idea that it’s going to be notifying observers until runtime.
Something that I didn’t realize until after I published the post, just because something uses an @property
doesn’t mean it’s okay to observe with KVO. In this particular example, observing the table view’s contentSize
happens to work, but if the ivar was being manipulated underneath the hood, you wouldn’t get KVO notifications, and KVO would enter an inconsistent state.
Weak properties are KVOable, and this is made to work by the framework inserting an intermediary observer, which is not cleaned up when the __weak
object gets deallocated. This causes you to attempt to access a dereferenced pointer, which causes a crash. This is a known bug. You also don’t get a notification when the weak pointer is nilled out, reducing some of the usefulness of KVO.
Back in 2005, the observer pattern was the way to go for model changes. Everyone wrote Java: controls would update models, which would be listened to by controllers, and the pattern reigned. At some point, programmers began to see the shortcomings in this pattern, and started using more explicit patterns, such as delegation, closure callbacks, and more explicit pub/sub (like NSNotificationCenter
, which of course has some of these flaws, but at least has a decent API that doesn’t screw you up at every turn).
The evidence of KVO being a relic of times gone by is plentiful. For example, when making apps for the Mac, you can use a tool called Cocoa Bindings. Bindings are a tool that use KVO to “bind” part of an object, like a property called “name” to a text field that would be used to edit the “name” property. Since these are “bound” together, changes to the model would cause the text field to update, and changes to the text field would cause the model to update.
This sounds useful, but it doesn’t exist in the Cocoa Touch API, for a few reasons. Bindings were slow, bindings are hard to debug because of their implicit nature, and with fewer view controllers on screen, they are less important. The movement away from this pattern is telling, though.
When I program, I consider only 2 cases acceptable for KVO.
When Apple requires it. A great example of this is in the AVPlayer
class. Apple requires in the documentation that you observe the status
property to be given info on when the player’s status is AVPlayerStatusReadyToPlay
. Like in scenario 1, you’re given no other option and have to observe to get this information out. In cases like this, I usually wrap it up in another class, possibly with a delegate or a block callback, and use that instead.
Designing an API for someone else. If you are designing a tool for public use, and you want to be informed of scrollViewDidScroll:
notifications, but you don’t want to prevent the users of your library from being the delegate of the scrollview, you can observe contentOffset
instead. This can run you afoul of KVOing on properties that aren’t explicitly documented as KVO-able, but it can’t be helped.