Dear Cocoa Hackers,
I'm building a document-based Cocoa application that works like most others except that I am using SQLCipher directly as my data store instead of Core Data. I'd love to use Core Data, but there is no API for creating your own persistent store (only an atomic store). Furthermore, I need control of the database schema for our replication system, so I'd probably still be working on this if an API for creating custom persistent data stores became available.
In any event, I'm trying to lean on Cocoa to do as much of my work for me as possible with bindings and the document model, and it's working out pretty well so far except that I have the problem of knowing when it's time to save changes made in the data model to the store. To be specific:
In my document sub-class, I've got an NSMutableArray property named categories. In the document nib I've got an NSArrayController bound to categories, and I've got an NSCollectionView bound to the array controller.
Each of my model objects in the array (each is of class `Category`) is bound to a record in the underlying data store, so when some property of a Category changes, I want to call `[category save]`, when a Category is added to the set, I want to call, again, `[category save]`, and finally, when a category is removed, `[category destroy]`.
I've wired up a partial solution, which properly saves the items when they added or edited in the interface, but it falls apart on the removal requirement, and everything about it seems to me as though I'm barking up the wrong tree. Anyway, here's what's going on:
Once the document and nib are all loaded up, I start observing the categories property, and assign it some data:
[self addObserver:self
forKeyPath:@"categories"
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:MyCategoriesContext];
self.categories = [Category getCategories];
I've implemented the observation method in such a way as that I am informed of changes so that the document can respond and update the data store.
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
NSNumber *changeKind = (NSNumber *)[change objectForKey:@"NSKeyValueChangeKind"];
if (context == MyCategoriesContext)
{
switch ([changeKind intValue])
{
case NSKeyValueChangeInsertion:
{
Category *c = (Category *)[change objectForKey:NSKeyValueChangeNewKey];
NSLog(@"saving new category: %@", c);
[c save];
break;
}
case NSKeyValueChangeRemoval:
{
Category *c = (Category *)[change objectForKey:NSKeyValueChangeOldKey];
NSLog(@"deleting removed category: %@", c);
[c destroy];
break;
}
case NSKeyValueChangeReplacement:
{
// not a scenario we're interested in right now...
NSLog(@"category replaced with: %@", (Category *)[change objectForKey:NSKeyValueChangeNewKey]);
break;
}
default: // gets hit when categories is set directly to a new array
{
NSLog(@"categories changed, observing each");
NSMutableArray *categories = (NSMutableArray *)[object valueForKey:keyPath];
NSIndexSet *allIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [categories count])];
[self observeCategoriesAtIndexes:allIndexes];
break;
}
}
}
else if (context == MyCategoryContext)
{
NSLog(@"saving category for change to %@", keyPath);
[(Category *)object save];
}
else
{
// pass it on to NSObject/super since we're not interested
NSLog(@"ignoring change to %@:@%@", object, keyPath);
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
As you can see from that listing (and as you might already be aware), it's not enough to observe the `categories` property, I need to observe each individual category so that the document is notified when it's attributes have been changed (like the name) so that I can save that change immediately:
- (void)observeCategoriesAtIndexes:(NSIndexSet *)indexes {
[categories addObserver:self
toObjectsAtIndexes:indexes
forKeyPath:@"dirty"
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:MyCategoryContext];
}
This looks to me like a big kludge, and I suspect I'm working against Cocoa here, but for the most part it works.
Except for removal. When you add a button to your interface, and assign it to the array controller's `remove:` action, it will properly remove the category from the `categories` property on my document. In doing so, the category is deallocated while it is still under observation:
2010-09-03 13:51:14.289 MyApp[7207:a0f] An instance 0x52db80 of class Category was deallocated while key value observers were still registered with it. Observation info was leaked, and may even become mistakenly attached to some other object. Set a breakpoint on NSKVODeallocateBreak to stop here in the debugger. Here's the current observation info:
<NSKeyValueObservationInfo 0x52e100> (
<NSKeyValueObservance 0x2f1a480: Observer: 0x2f0fa00, Key path: dirty, Options: <New: YES, Old: YES, Prior: NO> Context: 0x1a67b4, Property: 0x2f1a3d0>
...
)
In addition, because the object has been deallocated before I've been notified, I don't have the opportunity to call `[category destroy]` from my observer.
How is one supposed to properly integrate with NSArrayController to persist changes to the data model pre-Core Data? How would one work-around the remove problem here (or is this the wrong approach entirely?) I've done a lot of digging, and the examples I can find that don't involve Core Data use NSKeyedArchiver, which wouldn't work for my setup.
Thanks in advance for any advice!
Billy Gray
--
Team Zetetic
http://zetetic.net