Key-Value Observing
Ask anyone who’s been around the NSBlock a few times: Key-Value Observing has the worst API in all of Cocoa. It’s awkward, verbose, and confusing. And worst of all, its terrible API belies one of the most compelling features of the framework.
When dealing with complicated, stateful systems, dutiful book-keeping is essential for maintaining sanity. Lest the left hand not know what the right hand doeth, objects need some way to publish and subscribe to state changes over time.
In Objective-C and Cocoa, there are a number of ways that these events are communicated, each with varying degrees of formality and coupling:
-
NSNotification
&NSNotification
provide a centralized hub through which any part of an application may notify and be notified of changes from any other part of the application. The only requirement is to know what to look for, specifically in the name of the notification. For example,Center UIApplication
signals a low memory environment in an application.Did Receive Memory Warning Notification -
Key-Value Observing allows for ad-hoc, evented introspection between specific object instances by listening for changes on a particular key path. For example, a
UIProgress
might observe theView number
of a network request to derive and update its ownOf Bytes Read progress
property. -
Delegates are a popular pattern for signaling events over a fixed set of methods to a designated handler. For example,
UIScroll
sendsView scroll
to its delegate each time its scroll offset changes.View Did Scroll: -
Callbacks of various sorts, whether block properties like
NSOperation -completion
, which trigger afterBlock is
, or C function pointers passed as hooks into functions likeFinished == YES SCNetwork
.Reachability Set Callback(3)
Of all of these methods, Key-Value Observing is arguably the least well-understood. So this week, NSHipster will endeavor to provide some much-needed clarification and notion of best practices to this situation. To the casual observer, this may seem an exercise in futility, but subscribers to this publication know better.
<NSKey
, or KVO, is an informal protocol that defines a common mechanism for observing and notifying state changes between objects. As an informal protocol, you won’t see classes bragging about their conformance to it (it’s just implicitly assumed for all subclasses of NSObject
).
The main value proposition of KVO is rather compelling: any object can subscribe to be notified about state changes in any other object. Most of this is built-in, automatic, and transparent.
For context, similar manifestations of this observer pattern are the secret sauce of most modern Javascript frameworks, such as Backbone.js and Ember.js.
Subscribing
Objects can have observers added for a particular key path, which, as described in the KVC operators article, are dot-separated keys that specify a sequence of properties. Most of the time with KVO, these are just the top-level properties on the object.
The method used to add an observer is –add
:
- (void)add Observer:(NSObject *)observer
for Key Path:(NSString *)key Path
options:(NSKey Value Observing Options)options
context:(void *)context
observer
: The object to register for KVO notifications. The observer must implement the key-value observing method observeValueForKeyPath:ofObject:change:context:.key
: The key path, relative to the receiver, of the property to observe. This value must not bePath nil
.options
: A combination of theNSKey
values that specifies what is included in observation notifications. For possible values, see “NSKeyValueObservingOptions”.Value Observing Options context
: Arbitrary data that is passed toobserver
inobserve
.Value For Key Path:of Object:change:context:
Yuck. What makes this API so unsightly is the fact that those last two parameters are almost always 0
and NULL
, respectively.
options
refers to a bitmask of NSKey
. Pay particular attention to NSKey
& NSKey
as those are the options you’ll most likely use, if any. Feel free to skim over NSKey
& NSKey
:
NSKeyValueObservingOptions
NSKey
: Indicates that the change dictionary should provide the new attribute value, if applicable.Value Observing Option New NSKey
: Indicates that the change dictionary should contain the old attribute value, if applicable.Value Observing Option Old NSKey
: If specified, a notification should be sent to the observer immediately, before the observer registration method even returns. The change dictionary in the notification will always contain anValue Observing Option Initial NSKey
entry ifValue Change New Key NSKey
is also specified but will never contain anValue Observing Option New NSKey
entry. (In an initial notification the current value of the observed property may be old, but it’s new to the observer.) You can use this option instead of explicitly invoking, at the same time, code that is also invoked by the observer’sValue Change Old Key observe
method. When this option is used withValue For Key Path:of Object:change:context: add
a notification will be sent for each indexed object to which the observer is being added.Observer:for Key Path:options:context: NSKey
: Whether separate notifications should be sent to the observer before and after each change, instead of a single notification after the change. The change dictionary in a notification sent before a change always contains anValue Observing Option Prior NSKey
entry whose value isValue Change Notification Is Prior Key @YES
, but never contains anNSKey
entry. When this option is specified the change dictionary in a notification sent after a change contains the same entries that it would contain if this option were not specified. You can use this option when the observer’s own key-value observing-compliance requires it to invoke one of theValue Change New Key -will
methods for one of its own properties, and the value of that property depends on the value of the observed object’s property. (In that situation it’s too late to easily invokeChange... -will
properly in response to receiving anChange... observe
message after the change.)Value For Key Path:of Object:change:context:
These options allow an object to get the values before and after the change. In practice, this is usually not necessary, since the new value is generally available from the current value of the property.
That said, NSKey
can be helpful for reducing the code paths when responding to KVO events. For instance, if you have a method that dynamically enables a button based on the text
value of a field, passing NSKey
will have the event fire with its initial state once the observer is added.
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 bit complicated, and will be discussed later.
Responding
Another aspect of KVO that lends to its ugliness is the fact that there is no way to specify custom selectors to handle observations, as one might be used to from the Target-Action pattern used by controls.
Instead, all changes for observers are funneled through a single method—-observe
:
- (void)observe Value For Key Path:(NSString *)key Path
of Object:(id)object
change:(NSDictionary *)change
context:(void *)context
Those parameters are the same as what were specified in –add
, with the exception of change
, which are populated from whichever NSKey
options
were used.
A typical implementation of this method looks something like this:
- (void)observe Value For Key Path:(NSString *)key Path
of Object:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([key Path is Equal To String:@"state"]) {
…
}
}
Depending on how many kinds of objects are being observed by a single class, this method may also introduce -is
or -responds
in order to definitively identify the kind of event being passed. However, the safest method is to do an equality check to context
—especially when dealing with subclasses whose parents observe the same keypath.
Correct Context Declarations
What makes a good context
value? Here’s a suggestion:
static void * XXContext = &XXContext;
It’s that simple: a static value that stores its own pointer. It means nothing on its own, which makes it rather perfect for <NSKey
:
- (void)observe Value For Key Path:(NSString *)key Path
of Object:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if (context == XXContext) {
if ([key Path is Equal To String:NSString From Selector(@selector(is Finished))]) {
}
}
}
Better Key Paths
Passing strings as key paths is strictly worse than using properties directly, as any typo or misspelling won’t be caught by the compiler, and will cause things to not work.
A clever workaround to this is to use NSString
and a @selector
literal value:
NSString From Selector(@selector(is Finished))
Since @selector
looks through all available selectors in the target, this won’t prevent all mistakes, but it will catch most of them—including breaking changes made by Xcode automatic refactoring.
- (void)observe Value For Key Path:(NSString *)key Path
of Object:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([object is Kind Of Class:[NSOperation class]]) {
if ([key Path is Equal To String:NSString From Selector(@selector(is Finished))]) {
}
} else if (...) {
…
}
}
Unsubscribing
When an observer is finished listening for changes on an object, it is expected to call –remove
. This will often either be called in -observe
, or -dealloc
(or a similar destruction method).
@try
/ @catch
Safe Unsubscribe with Perhaps the most pronounced annoyance with KVO is how it gets you at the end. If you make a call to –remove
when the object is not registered as an observer (whether because it was already unregistered or not registered in the first place), an exception is thrown. The kicker is that there’s no built-in way to even check if an object is registered!
Which causes one to rely on a rather unfortunate cudgel @try
with an unhandled @catch
:
- (void)observe Value For Key Path:(NSString *)key Path
of Object:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([key Path is Equal To String:NSString From Selector(@selector(is Finished))]) {
if ([object is Finished]) {
@try {
[object remove Observer:self for Key Path:NSString From Selector(@selector(is Finished))];
}
@catch (NSException * __unused exception) {}
}
}
}
Granted, not handling a caught exception, as in this example, is waving the [UIColor white
flag of surrender. Therefore, one should only really use this technique when faced with intermittent crashes which cannot be remedied by normal book-keeping (whether due to race conditions or undocumented behavior from a superclass).
Automatic Property Notifications
KVO is made useful by its near-universal adoption. Because of this, much of the work necessary to get everything hooked up correctly is automatically taken care of by the compiler and runtime.
Classes can opt-out of automatic KVO by overriding
+automatically
and returningNotifies Observers For Key: NO
.
But what about compound or derived values? Let’s say you have an object with a @dynamic
, readonly
address
property, which reads and formats its street
, locality
, region
, and postal
?
Well, you can implement the method key
(or its less magical catch-all, +key
):
+ (NSSet *)key Paths For Values Affecting Address {
return [NSSet set With Objects:NSString From Selector(@selector(street Address)), NSString From Selector(@selector(locality)), NSString From Selector(@selector(region)), NSString From Selector(@selector(postal Code)), nil];
}
So there you have it: some general observations and best practices for KVO. To an enterprising NSHipster, KVO can be a powerful substrate on top of which clever and powerful abstractions can be built. Use it wisely, and understand the rules and conventions to make the most of it in your own application.