Using Key-Value Observing (KVO) with Core Data is not the easiest relationship to manage. But in a specific use case, such as automatically updating an attribute on a model entity when another attribute changes, it can be more straightforward.
Consider the scenario where you wish to keep track of which managed objects have been modified so that you can sync them with a remote database. You would like to maintain a property like stale that is set automatically when the other properties are changed. It turns out that KVO is the most direct way to do this. I’ll show you how, play-by-play.
First consider a simple model of an entity Foo that has properties for boo, moo, and stale.
The associated class interface for Foo:
1
2
3
4
5
6
7
8
9
| @interface Foo : NSManagedObject
{
}
@property (nonatomic, retain) NSNumber * stale;
@property (nonatomic, retain) NSNumber * boo;
@property (nonatomic, retain) NSString * moo;
@end |
For this simple example, we just want to set the stale property whenever boo or moo changes. That way, when it comes time to sync with our remote store, when can select only the stale entities. This is where KVO comes to the rescue. First, we need to register stale’s dependent key; and the method you need is:
1
2
| // return an NSSet with the keypaths on which our key (stale) depends
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key; |
In our case, here’s how we register the dependent keys:
1
2
3
4
5
6
7
8
9
10
11
12
| + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key;
{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
// if the key is @"stale" add our specific affecting keys to those returned by super
if( [key isEqualToString:@"stale"] )
{
NSSet *affectingKeys = [NSSet setWithObjects:@"boo",@"moo",nil];
keyPaths = keyPaths = [keyPaths setByAddingObjectsFromSet:affectingKeys];
}
return keyPaths;
} |
To respond to changes in stale (via moo and boo) we implement:
1
2
3
4
5
6
7
8
| - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
{
if( [keyPath isEqualToString:@"stale"] )
{
NSLog(@"stale change - %@",change);
[self setPrimitiveValue:[NSNumber numberWithBool:YES] forKey:@"stale"];
}
} |
Note that we use -setPrimitiveValue:forKey: in this case. Why? Because, otherwise we set up an endless loop where changes in stale trigger KVO. From Apple’s documentation of -setPrimitiveValue:forKey:
Sets in the receiver’s private internal storage the value of the property specified by key to value. If key identifies a to-one relationship, relates the object specified by value to the receiver, unrelating the previously related object if there was one. Given a collection object and a key that identifies a to-many relationship, relates the objects contained in the collection to the receiver, unrelating previously related objects if there were any.
This method does not invoke the change notification methods (willChangeValueForKey: and didChangeValueForKey:). It is typically used by subclasses that implement custom accessor methods that need direct access to the receiver’s private internal storage. It is also used by the Core Data framework to initialize the receiver with values from a persistent store or to restore a value from a snapshot.
Importantly, it doesn’t invoke the change notification methods used by KVO.
Lastly, we have to register ourselves as observers for the property stale.
1
2
3
4
5
6
7
8
9
| - (void)awakeFromInsert;
{
[self addObserver:self forKeyPath:@"stale" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)awakeFromFetch;
{
[self addObserver:self forKeyPath:@"stale" options:NSKeyValueObservingOptionNew context:NULL];
} |
That’s it! Now, whenever boo and moo properties are changed, stale is set accordingly.