DanielGibbs
DanielGibbs

Reputation: 10183

Why doesn't my KVO dependency not work in NSArrayController

I would like to use an NSArrayController with an NSTableView to allow multiple selection but only provided a selected object when a single object is selected (and nil when none or multiple are selected).

I've attempted to implement this with a category on NSArrayController, as shown here:

@implementation NSArrayController (SelectedObject)

+ (NSSet *)keyPathsForValuesAffectingSelectedObject {
    return [NSSet setWithObject:@"selection"];
}

- (id)selectedObject {
    // Get the actual selected object (or nil) instead of a proxy.
    if (self.selectionIndexes.count == 1) {
        return [self arrangedObjects][self.selectionIndex];
    }
    return nil;
}

@end

For some reason, the selectedObject method is not called when the selection of the array controller changes (and something else is observing selectedObject). Why is this?

Upvotes: 2

Views: 346

Answers (2)

DanielGibbs
DanielGibbs

Reputation: 10183

I managed to get this working by creating a subclass of NSArrayController and manually observing the selectionIndexes key. I'd prefer to do it using a category but this does appear to work.

static NSString *const kObservingSelectionIndexesContext = @"ObservingSelectionIndexesContext";

@implementation BetterArrayController

- (void)awakeFromNib {
    [super awakeFromNib];
    [self addObserver:self forKeyPath:@"selectionIndexes" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew context:(void *)&kObservingSelectionIndexesContext];
}

- (void)dealloc {
    [self removeObserver:self forKeyPath:@"selectionIndexes" context:(void *)&kObservingSelectionIndexesContext];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context == (void *)&kObservingSelectionIndexesContext) {
        [self willChangeValueForKey:@"selectedObject"];
        [self didChangeValueForKey:@"selectedObject"];
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

- (id)selectedObject {
    // Get the actual selected object (or nil) instead of a proxy.
    if (self.selectionIndexes.count == 1) {
        return [self arrangedObjects][self.selectionIndex];
    }
    return nil;
}

@end

I used a context (as per this article) to avoid removing any observers the superclass may have in dealloc (as cautioned against here).

Upvotes: 0

Ken Thomases
Ken Thomases

Reputation: 90521

The selection property of NSArrayController is strange voodoo. I don't know if key-value observing it (and not a path that goes through it) produces change notifications when the selection changes. After all, it returns a proxy and there's no reason to believe that the identity of that proxy changes over time.

In any case, your actual selectedObject method doesn't actually use selection (and it shouldn't). It uses arrangedObjects and selectionIndexes. So, you should return a set containing those keys from +keyPathsForValuesAffectingSelectedObject.

Of course, if you're using a view-based table, you need to make sure the table view's selectionIndexes binding is bound to the array controller's selectionIndexes property, or the array controller just won't know anything about the selection in the table view. (For cell-based table views, you'd typically bind the columns to the array controller and the table view would automatically bind its own bindings based on the columns' bindings.)

Finally, I think you should choose a different name for selectedObject. It's too likely that Apple has a private method of that name or will add one in the future.

Upvotes: 1

Related Questions