0
votes

A xib file in my OS X project contains a table view backed by an NSArrayController which is set to Entity Name (aka Core Data) mode. When I send an -add: message to this array controller, an exception is raised as the console emits:

NSManagedObjects of entity 'Foo' do not support -mutableArrayValueForKey: for the property 'bars'.

Correct! Instead, NSManagedObjects support -mutableSetValueForKey:, and I'd always thought that NSArrayController knew this and behaved accordingly – being in Entity Name mode, I expect it to send -mutableSetValueForKey: to my Foo object, not -mutableArrayValueForKey:. From the documentation:

Typically the collection is an array, however, if the controller manages a relationship of a managed object (see NSManagedObject) the collection may be a set.

If I "fake it" by adding the following method to my Foo class, it prevents the exception from being raised but, of course, this doesn't really work:

- (NSMutableArray*)mutableArrayValueForKey:(NSString*)key {
    [self willAccessValueForKey:key];

    NSMutableArray* result = [[[self valueForKey:key] allObjects] mutableCopy] ;

    [self didAccessValueForKey:key];
    return result;
}

What might be wrong here? I've checked the "mode" of that array controller five times.

1

1 Answers

0
votes

After additional research, I think that this issue is part of the larger problem of NSOrderedSet not being compatible with Cocoa Bindings. That issue is explained here by Tom Fewster:

However, as commenter Rick explains in comments to that blog post, Tom Fewster's OrderedSetArrayValueTransformer still does not solve the problem with add: and remove:.

I therefore wrote a very kludgey works for now workaround, subclassing NSArrayController and overriding -add: and -remove:. My implementations reach out to the Foo parent object, which owns the Bar collection (bars). Normally, this object is not available to the array controller. I get it by following the inverse relationship from the first Bar object, which works because, in this particular application, Foo must always have at least one Bar. Otherwise, we would need a separate "wire" (property, or preferably a binding which Interface Builder would not support) to the Foo object. Arghhhh.

Please note that the following code is a really bad solution to a bad problem. I hope it may inspire someone to post a better idea; but better than Haha, don't use Cocoa Bindings and/or Core Data! because my ship has already sailed on that, especially in this particular project.

This code uses collection accessor methods which your managed objects may not have. The easiest way to get them is to use mogenerator.

@implementation BarArrayController

- (Foo*)parentObject {
    Bar* aBar = (Bar*)[[self content] firstObject] ;
    NSAssert(aBar, @"Cannot have 0 bars") ;
    Foo* parentObject = aBar.Foo ;

    return parentObject ;
}

- (void)add:(id)sender {
    NSManagedObject* newObject = [NSEntityDescription insertNewObjectForEntityForName:self.entityName
                                                               inManagedObjectContext:self.managedObjectContext] ;
    Foo* parentObject = [self parentObject] ;
    [parentObject insertObject:(Bar*)newObject
                 inBarsAtIndex:parentObject.slices.count];
}

- (void)remove:(id)sender {
    Foo* parentObject = [self parentObject] ;
    if (parentObject.bars.count > 1) {
        NSInteger selectedIndex = self.selectionIndex ;
        if (selectedIndex != NSNotFound) {
            NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:selectedIndex] ;
            [parentObject removeBarsAtIndexes:indexSet] ;
        }
    }
    else {
        NSBeep() ;
    }
}

@end