Persisting a document's data without Core Data or NSKeyedArchiver

97 views
Skip to first unread message

Billy Gray

unread,
Sep 7, 2010, 11:15:25 AM9/7/10
to cocoa-...@googlegroups.com
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

Bill Garrison

unread,
Sep 7, 2010, 12:05:55 PM9/7/10
to cocoa-...@googlegroups.com

On Sep 7, 2010, at 11:15 AM, Billy Gray wrote:

> case NSKeyValueChangeRemoval:
> {
> Category *c = (Category *)[change objectForKey:NSKeyValueChangeOldKey];
> NSLog(@"deleting removed category: %@", c);
> [c destroy];
> break;
> }

It might make sense to remove the 'dirty' property observer on the individual Category here, at the point where it it going away.

case NSKeyValueChangeRemoval:
{
[c removeObserver:self forKeyPath:@"dirty"];

Category *c = (Category *)[change objectForKey:NSKeyValueChangeOldKey];
NSLog(@"deleting removed category: %@", c);
[c destroy];
break;
}

-------

Alternately, you could subclass NSArrayController to make one specifically geared to handling Category objects backed by SQLCipher.

I've taken this approach in other apps and it worked well. You're capturing changes to the controlled array a little closer to the source than when using bindings.

Override NSArrayController's -(void)removeObject:(id)object and do your object clean up there.

- (void) removeObject: (id)object {
[super removeObject: object]; // ensures that all KVO notifications are sent out first.

Category *c = (Category *)object;


NSLog(@"deleting removed category: %@", c);
[c destroy];
}

This moves the Category/SQLCipher logic out of your document class and into a NSArrayController subclass. Maybe that's not appropriate in your app. But it also frees you from having to use bindings for managing SQLCipher interaction, which could be a bonus.

Bill


Billy Gray

unread,
Sep 7, 2010, 12:42:10 PM9/7/10
to cocoa-...@googlegroups.com
On Tue, Sep 7, 2010 at 12:05 PM, Bill Garrison <1billg...@gmail.com> wrote:
It might make sense to remove the 'dirty' property observer on the individual Category here, at the point where it it going away.

                       case NSKeyValueChangeRemoval:
                       {
                               [c removeObserver:self forKeyPath:@"dirty"];

                               Category *c = (Category *)[change objectForKey:NSKeyValueChangeOldKey];
                               NSLog(@"deleting removed category: %@", c);
                               [c destroy];
                               break;
                       }

Yeah, that's a good point, as that would kick off another error if I had the whole mess working. Ironically, the delayed nature of the notification is such that this message is never sent, thanks to the deallocation in the NSArrayController which then kicks off the previously-mentioned KVO error.

 

This moves the Category/SQLCipher logic out of your document class and into a NSArrayController subclass.  Maybe that's not appropriate in your app.  But it also frees you from having to use bindings for managing SQLCipher interaction, which could be a bonus.

Thanks, Bill, I'm going to give this a shot next. Certainly strikes me as a cleaner way of going about it.

Cheers,
Billy
Reply all
Reply to author
Forward
0 new messages