views:

71

answers:

2

I hope you'll excuse the seemingly broad nature of this question, but it gets quite specific.

I'm building a document-based Cocoa application that works like most others except that I am using SQLCipher for my data store (a variant of SQLite), because you don't get to set your own persistent data store in Core Data, and also I really need to use this one.

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 a 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, 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?)

Thanks in advance for any advice!

+1  A: 

I would observe changes to categories list, and when the list changes, store the array of categories away in a secondary NSArray, 'known categories', using mutableCopy. Next time the list changes, compare that 'known' list to the new list; you can tell which categories are missing, which are new, etc. For each removed category, stop observing it and release it.

Then take a new mutable copy for the 'known' list of categories, ready for the next call.

Since you have an additional array holding the categories, they aren't released before you're ready.

Graham Perks
Not a bad work-around!
Billy Gray
I found that in an old Apple sample, so it has some level of blessing.
Graham Perks
A: 

It would seem, based on some initial hacking, that subclassing NSArrayController is the way to go here. Over-riding the various insertObject(s) and removeObject(s) methods in that API gives me the perfect place to add in this logic for messing with the data model.

And from there I can also begin to observe the individual items in the content array for changes, etc, stop observation before destroying/deallocating them, etc, and let the parent class handle the rest.

Thanks for this solution is due to Bill Garrison who suggested it on the cocoa-unbound list.

Billy Gray