Key–Value Observing
Key–value observing, or KVO, is a mechanism somewhat similar to the target–action mechanism, except that it is not limited to controls. (The KVO mechanism is provided through an informal protocol, NSKeyValueObserving, which is actually a set of categories on NSObject and other classes.) The similarity is that objects register with a particular object to be notified when something happens. The “something” is that a certain value in that object is changed.
KVO can be broken down into three stages:
Registration
To hear about a change in a value belonging to object A, object B must be registered with object A. Change
The change takes place in the value belonging to object A, and it must take place in a special way — a KVO compliant way.
Notification
Object B is notified that the value in object A has changed and can respond as desired.
example:
// [In MyClass1.h]
@interface MyClass1 : NSObject {
NSString* value;
}
@property (nonatomic, copy) NSString* value;
@end
// [In MyClass1.m]
@implementation MyClass1
@synthesize value;
@end
// [In MyClass2.m (in its implementation section)]
- (void) observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
NSLog(@"I heard about the change!");
}
// [somewhere else entirely]
MyClass1* objectA = [[MyClass1 alloc] init];
MyClass2* objectB = [[MyClass2 alloc] init];
// register for KVO
[objectA addObserver:objectB forKeyPath:@"value" options:0 context:nil];
// change the value in a KVO compliant way
objectA.value = @"Hello, world!";
// result: objectB's observeValueForKeyPath:... is called
We call addObserver:forKeyPath:options:context: to register objectB to hear about changes in objectA’s value. We didn’t use any options or context; I’ll talk about the options in a moment. (The context is for handing in a value that will be provided as part of the notification.) We change objectA’s value, and we do it in a KVO compliant way, namely, by passing through the setter (because setting a property is equivalent to passing through the setter). This is another reason why accessors (and properties) are a good thing: they help you guarantee KVO compliance when changing a value.
When we change objectA’s value, the third stage takes place automatically: a call is made to objectB’s observeValueForKeyPath:.... We have implemented this method in MyClass2 in order to receive the notification. In this simple example, we expect to receive only one notification, so we just log to indicate that we did indeed receive it. In real life, where a single object might be registered to receive more than one KVO notification, you’d use the incoming parameters to distinguish between different notifications and decide what to do.
At the very least, you’ll probably want to know, when observeValueForKeyPath:... is called, what the new value is. We can find that out easily, because we are handed a reference to the object that changed, along with the key path for the value within that object. Thus we can use KVC to query the changed object in the most general way:
- (void) observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
id newValue = [object valueForKeyPath:keyPath];
NSLog(@"The key path %@ changed to %@", keyPath, newValue);
}
But it is also possible to request that the new value be included as part of the notification. This depends upon the options passed with the original registration. Here, we’ll request that both the old and new values be included with the notification:
objectA.value = @"Hello";
[objectA addObserver:objectB forKeyPath:@"value"
options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
objectA.value = @"Goodbye"; // notification is triggered
When we receive the notification, we fetch the old and new values out of the change dictionary:
- (void) observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
id newValue = [change objectForKey: NSKeyValueChangeNewKey];
id oldValue = [change objectForKey: NSKeyValueChangeOldKey];
NSLog(@"The key path %@ changed from %@ to %@", keyPath, oldValue, newValue);
}
No memory management happens as part of the registration process, so it is incumbent upon you to unregister object B before it is destroyed. Otherwise, object A may later attempt to send a notification to a dangling pointer. This is done by sending object A the removeObserver:forKeyPath: message.
To be notified when an object is added to, removed from, or replaced within the array. You can’t add an observer to an array itself; you have to observe through an object that has a key path to the array (through accessors, for example). The simple-minded solution is then to access the array using mutableArrayValueForKey:, which provides an observable proxy object.
example:
(
{
description = "The one with glasses.";
name = Manny;
},
{
description = "Looks a little like Governor Dewey.";
name = Moe;
},
{
description = "The one without a mustache.";
name = Jack;
}
)
Suppose this is an NSMutableArray. Then we can register with our object to observe the key path @"theData":
[objectA addObserver:objectB forKeyPath:@"theData" options:0 context:nil];
Now object B will be notified of changes to this mutable array, but only if those changes are performed through the mutableArrayValueForKey: proxy object:
[[objectA mutableArrayValueForKeyPath:@"theData"] removeObjectAtIndex:0];
// notification is triggered
But it seems onerous to require clients to know that they must call mutableArrayValueForKey:. The simple solution is for our object itself to provide a getter that calls mutableArrayValueForKey:. Here’s a possible implementation:
// [In MyClass1, in the header file]
@interface MyClass1 : NSObject {
NSMutableArray* theData;
}
@property (nonatomic, retain, getter=theDataGetter) NSMutableArray* theData;
@end
// [In MyClass1, in the implementation section]
@synthesize theData;
- (NSMutableArray*) theDataGetter {
return [self mutableArrayValueForKey:@"theData"];
}
The result is that, as far as any client knows, this object has a key @"theData" and a property theData, and we can register to observe with the key and then access the mutable array through the property:
[objectA addObserver:objectB
forKeyPath:@"theData"
options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
[objectA.theData removeObjectAtIndex:0]; // notification is triggered
If you’re going to take this approach, you should really also implement (in MyClass1) the four KVC compliance methods for a mutable array façade. Although things will appear to work just fine without them, and although they appear trivial (they are merely delegating to self->theData the equivalent calls), they will be called by the vended proxy object, which increases its efficiency (and, some would argue, its safety). Without these methods, the proxy object resorts to setting the instance variable directly, replacing the entire mutable array, every time a client changes the mutable array:
- (NSUInteger) countOfTheData {
return [self->theData count];
}
- (id) objectInTheDataAtIndex: (NSUInteger) ix {
return [self->theData objectAtIndex: ix];
}
- (void) insertObject: (id) val inTheDataAtIndex: (NSUInteger) ix {
[self->theData insertObject:val atIndex:ix];
}
- (void) removeObjectFromTheDataAtIndex: (NSUInteger) ix {
[self->theData removeObjectAtIndex: ix];
}
If what you want to observe are mutations within an individual element of an array, things are more complicated. Suppose our array of dictionaries is an array of mutable dictionaries. To observe changes to the value of the @"description" key of any dictionary in the array, you’d need to register for that key with each dictionary in the array, separately. You can do that efficiently with NSArray’s instance method addObserver: toObjectsAtIndexes:forKeyPath:options:context:, but if the array itself is mutable then you’re also going to have to register for that key with any new dictionaries that are subsequently added to the array (and unregister when a dictionary is removed from the array). This is doable but daunting.
The properties of Apple’s built-in classes are typically KVO compliant. Indeed, so are many classes that don’t use properties per se; for example, NSUserDefaults is KVO compliant. Unfortunately, Apple warns that undocumented KVO compliance can’t be counted on.