Search code examples
cocoakey-value-observingnsarraycontroller

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?


Solution

  • 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).