IOS4 note 14 (4) Key–Value Observing
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 registeredwith object A.Change
The change takes place in the value belonging to object A, and it must take placein a special way — a KVO compliant way.
Notification
Object B is notified that the value in object A has changed and can respond asdesired.
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 aboutchanges in objectA’s value. We didn’t use any options or context; I’ll talk about theoptions in a moment. (The context is for handing in a value that will be provided aspart 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 throughthe 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.
?