Introduction
If you've ever worked with KVO (Key-Value Observing) in Cocoa, chances are that you've run into various kinds of issues. The API isn't great and forgetting to remove an observer may result in memory leaks or—even worse—crashes. Facebook's KVOController library aims to solve this problem.
What Is Key-Value Observing?
If you're new to key-value observing or KVO, I recommend that you first read Apple's developer guide on the topic or Mattt Thompson's article on NSHipster. To quote Apple's guide on KVO, "Key-value observing provides a mechanism that allows objects to be notified of changes to specific properties of other objects." Mattt defines KVO as follows, "Key-Value Observing allows for ad-hoc, evented introspection between specific object instances by listening for changes on a particular key path." The keywords are evented and key path.
Before we discuss the KVOController library, I'd like to take a moment to explore the KVO API.
Adding an Observer
I won't cover KVO in great detail in this tutorial, but it's important that you understand the core concept of KVO. The idea is simple. An object can listen to changes to specific properties of another object. The observing object is added by the target object as an observer for a specific key path.
Let's illustrate this with an example. If objectB
wishes to be notified when the name
property of objectA
changes, then objectA
needs to add objectB
as an observer for the key path name
. Thanks to Objective-C's verbosity, the code to accomplish this is pretty simple.
[objectA addObserver:objectB forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
Responding to Changes
Whenever objectA
's name
property changes, observeValueForKeyPath:ofObject:change:context:
is invoked. The first parameter is the key path that's being observed by objectB
, the second parameter is the object of the key path, the third argument is a dictionary describing the changes, and the final argument is the context that was passed as the final argument of addObserver:forKeyPath:options:context:
.
I hope you agree that this isn't very elegant. If you're making extensive use of KVO, the implementation of observeValueForKeyPath:ofObject:change:context: quickly becomes long and complex.
Removing an Observer
It's important to stop listening for changes when an object is no longer interested in receiving notifications for a specific key path. This is done by invoking removeObserver:forKeyPath:
or removeObserver:forKeyPath:context:
.
The issue that every developer runs into at some point is either forgetting to call removeObserver:forKeyPath:
or calling removeObserver:forKeyPath:
with a key path that isn't being observed by the observer. The reasons for this are manyfold and are the root of the problem many developers face when working with KVO.
If you forget to invoke removeObserver:forKeyPath:
, you may end up with a memory leak. If you invoke removeObserver:forKeyPath:
and the object isn't registered as an observer an exception is thrown. The cherry on the cake is that the NSKeyValueObserving
protocol doesn't provide a way to check if an object is observing a particular key path.
KVOController to the Rescue
Luckily, the Cocoa team at Facebook was just as annoyed by the above issues as you are and they came up with a solution, the KVOController library. Instead of reinventing the wheel, the team at Facebook decided to build on top of KVO. Despite its shortcomings, KVO is robust, widely supported, and very useful.
The KVOController library adds a number of things to KVO:
- thread-safety
- painless removal of observers
- support for blocks and custom actions
Requirements
Before we get started, it's important to stress that the KVOController library requires ARC and that the minimum deployment targets are iOS 6 for iOS and 10.7 for OS X.
Installation
I'm a big proponent of CocoaPods and I hope you are too. To add the KVOController library to a project using CocoaPods, add the pod to your project's Podfile and run pod update
to install the library.
pod 'KVOController'
Alternatively, you can download the latest version of the library from GitHub and manually add the library by copying KVOController.h and KVOController.m to your project.
Examples
Initialization
The first thing you need to do is initialize an instance of the FBKVOController
class. Take a look at the following code snippet in which I create a FBKVOController
instance in a view controller's initWithNibName:bundle:
method.
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { _KVOController = [FBKVOController controllerWithObserver:self]; } return self; }
Note that I store a reference to the FBKVOController
object in the view controller's _KVOController
instance variable. A great feature of the KVOController library is that the observer is automatically removed the moment the FBKVOController
object is deallocated. In other words, there's no need to remember to remove the observer as this is automatically done the moment the FBKVOController
object is deallocated.
Adding an Observer
You have several options to start observing an object. You can take the traditional approach by invoking observe:keyPath:options:context:
. The result is that observeValueForKeyPath:ofObject:change:context:
is invoked whenever a change event takes place.
[_KVOController observe:person keyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
However, the FBKVOController
class also leverages blocks and custom actions as you can see in the following code snippets. I'm sure you agree that this makes working with KVO much more enjoyable.
[_KVOController observe:person keyPath:@"name" options:NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary *change) { // Respond to Changes }];
[_KVOController observe:person keyPath:@"name" options:NSKeyValueObservingOptionNew action:@selector(nameDidChange:)];
Removing an Observer
Even though the observer is automatically removed when the FBKVOController
object is deallocated, it's often necessary to stop observing an object before the observer is deallocated. The KVOController library has a number of methods to accomplish this simple task.
To stop observing a specific key path of an object, invoke unobserve:keyPath:
and pass the object and key path. You can also stop observing an object by invoking unobserve:
and pass in the object you want to stop observing. To stop observing every object, you can send the FBKVOController
object a message of unobserveAll
.
No Exceptions
If you take a look at the implementation of the FBKVOController
class, you'll notice that it keeps an internal map of the objects and key paths the observer is observing. The FBKVOController
class is more forgiving than the Apple's implementation of KVO. If you tell the FBKVOController
object to stop observing an object or key path that it isn't observing, no exception is thrown. That's how it should be.
Conclusion
Even though KVO isn't a difficult concept to grasp, making sure observers are properly removed and race conditions don't cause mayhem is the real challenge when working with KVO.
I encourage you to take a look at the KVOController library. However, I also advise you to get a good understanding of KVO before you use it in your projects so you know what this library is doing for you behind the scenes.