Search code examples
cocoabindingkey-value-observingnsarraycontrollerkvc

How to bind to NSArrayController's arranged objects


I would like to programmatically bind a custom class (MyClass) array to an array controller (NSArrayController) with its content bound to another array (modelArray). MyClass displays the content of the array, like a NSTableView.

My problem is: how to create this binding in such way that the mutable array's methods are called, that is the methods

-(void) insertObject:(id)object inContentAtIndex:(NSUInteger)index
-(void) removeObjectFromContent:(id) object

(1) If I bind in this way, the above methods are called but the controller's content is no longer bound to the modelArray (obviously)

[myArrayController bind:@"contentArray" toObject:myClassInstance withKeyPath:@"content" options:nil];

(2) If I bind in these ways only the setContent: and content methods are called and not the mutable methods. Also I've tried to remove those methods (setContent: and content) but it only raises an exception setValue:forUndefinedKey:

[myClassInstance bind:@"content" toObject:myArrayController withKeyPath:@"arrangedObjects" options:nil];

or

[myClassInstance bind:@"content" toObject:myArrayController withKeyPath:@"content" options:nil];

I don't believe that the whole table's array is re-set each time a line is added when bound to an array controller, and I'd like to have the same kind of binding.


Solution

  • The issue you're having has to do with how array values are treated by Key Value Coding. KVC has no concept of specific typing, so when you access an array value via KVC, it has no way of knowing that the returned array is mutable. It has to assume the worst (i.e. that the array is immutable). The way it normally handles this is by using a proxy object that acts like an NSMutableArray, but behind the scenes it takes your assumed-immutable array, makes a mutable copy, mutates the copy, then pushes the whole thing back in using the setter. (This is the behavior you're seeing -- the entire array is being replaced instead of being mutated in place.)

    The method that controls this functionality is - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key. There's potentially a LOT going on in that method, and I'll paste in the header comments for this method below to give the full story, but to make a long story short, if you want the NSArrayController to mutate your mutable array in place, the simplest approach is to add this override to the class that vends the modelArray property:

    - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key
    {
        if ([@"modelArray" isEqual: key])
        {
            // We know this is mutable, even if KVC doesn't!
            return self.modelArray;
        }
        return [super mutableArrayValueForKey:key];
    }
    

    The longer story is that there is a sequence of things that KVC looks for when trying to figure out how to handle collection mutations. They're explained, in detail, in NSKeyValueCoding.h. These are the comments for mutableArrayValueForKey:.

    Given a key that identifies an ordered to-many relationship, return a mutable array that provides read-write access to the related objects. Objects added to the mutable array will become related to the receiver, and objects removed from the mutable array will become unrelated.

    The default implementation of this method recognizes the same simple accessor methods and array accessor methods as -valueForKey:'s, and follows the same direct instance variable access policies, but always returns a mutable collection proxy object instead of the immutable collection that -valueForKey: would return. It also:

    1. Searches the class of the receiver for methods whose names match the patterns -insertObject:in<Key>AtIndex: and -removeObjectFrom<Key>AtIndex: (corresponding to the two most primitive methods defined by the NSMutableArray class), and (introduced in Mac OS 10.4) also -insert<Key>:atIndexes: and -remove<Key>AtIndexes: (corresponding to -[NSMutableArray insertObjects:atIndexes:] and -[NSMutableArray removeObjectsAtIndexes:]). If at least one insertion method and at least one removal method are found each NSMutableArray message sent to the collection proxy object will result in some combination of -insertObject:in<Key>AtIndex:, -removeObjectFrom<Key>AtIndex:, -insert<Key>:atIndexes:, and -remove<Key>AtIndexes: messages being sent to the original receiver of -mutableArrayValueForKey:. If the class of the receiver also implements an optional method whose name matches the pattern -replaceObjectIn<Key>AtIndex:withObject: or (introduced in Mac OS 10.4) -replace<Key>AtIndexes:with<Key>: that method will be used when appropriate for best performance.
    2. Otherwise (no set of array mutation methods is found), searches the class of the receiver for an accessor method whose name matches the pattern -set<Key>:. If such a method is found each NSMutableArray message sent to the collection proxy object will result in a -set<Key>: message being sent to the original receiver of -mutableArrayValueForKey:.
    3. Otherwise (no set of array mutation methods or simple accessor method is found), if the receiver's class' +accessInstanceVariablesDirectly method returns YES, searches the class of the receiver for an instance variable whose name matches the pattern _<key> or <key>, in that order. If such an instance variable is found, each NSMutableArray message sent to the collection proxy object will be forwarded to the instance variable's value, which therefore must typically be an instance of NSMutableArray or a subclass of NSMutableArray.
    4. Otherwise (no set of array mutation methods, simple accessor method, or instance variable is found), returns a mutable collection proxy object anyway. Each NSMutableArray message sent to the collection proxy object will result in a -setValue:forUndefinedKey: message being sent to the original receiver of -mutableArrayValueForKey:. The default implementation of -setValue:forUndefinedKey: raises an NSUndefinedKeyException, but you can override it in your application.