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 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.

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值