objective-ccocoanscollectionviewnscollectionviewitem

How to properly show the current selection in an NSCollectionView?


I have an NSCollectionView that is showing some images. I have implemented an NSCollectionViewDelegate to tell it which items should be selected and/or highlighted. I'm using a stock NSCollectionViewItem to draw the images and their names. When the user selects an item, my delegate gets the messages about highlight state changes:

- (void)collectionView:(NSCollectionView *)collectionView
didChangeItemsAtIndexPaths:(NSSet<NSIndexPath *> *)indexPaths
      toHighlightState:(NSCollectionViewItemHighlightState)highlightState
{
    [collectionView reloadItemsAtIndexPaths:indexPaths];
}

I do a similar thing for didSelect/didDeselect:

- (void)collectionView:(NSCollectionView *)collectionView
didSelectItemsAtIndexPaths:(nonnull NSSet<NSIndexPath *> *)indexPaths
{
    [collectionView reloadItemsAtIndexPaths:indexPaths];
}

In the NSCollectionViewItems view, I do the following:

- (void)drawRect:(NSRect)dirtyRect {
    [super drawRect:dirtyRect];

    NSColor*    bgColor         = [[self window] backgroundColor];
    NSColor*    highlightColor  = [NSColor selectedControlColor];

    NSRect  frame  = [self bounds];
    NSCollectionViewItemHighlightState  hlState     = [collectionViewItem highlightState];
    BOOL                                selected    = [collectionViewItem isSelected];
    if ((hlState == NSCollectionViewItemHighlightForSelection) || (selected))
    {
        [highlightColor setFill];
    }
    else
    {
        [bgColor setFill];
    }
    [NSBezierPath fillRect:frame];
}

The problem I'm seeing is that drawing the highlight or selection appears to be random. When it does draw the selection, it's almost always on the items the user has actually selected (though it often leaves off the last item for some reason). Occasionally, it will select a different item the user did not click on or drag over. Often, though, it just doesn't draw.

I've added printing to verify that it is calling -didChangeItemsAtIndexPaths:toHighlightState: and -didSelectItemsAtIndexPaths:. Is there anything I'm doing wrong here?

I've added some logging to the view's -drawRect: method, and it doesn't appear to be getting called on all transitions, even though I'm calling -reloadItemsAtIndexPaths: in the -didChange* methods. Why not?

I've also noticed that the delegate's -should/didDeselectItemsAtIndexPaths: does not seem to get called ever, even though the -should/didSelectItemsAtIndexPaths: does get called. Why is that?


Solution

  • The problem turned out to be calling [collectionView reloadItemsAtIndexPaths:]. When you do that, it removes the existing NSCollectionViewItem and creates a new one (by calling your data source's collectionView:itemForRepresentedObjectAt:). That immediately sets the new collection view item to not selected (or rather it doesn't set it to be selected). When that happens, it won't call your should/didDeselect methods because the existing item doesn't exist anymore, and the new one is not selected.

    The real solution turned out to be to subclass NSCollectionViewItem and override -setSelected: to do the following:

    - (void)setSelected:(BOOL)selected
    {
        [super setSelected:selected];
        [self.view setNeedsDisplay:YES];
    }
    

    When the view's -drawRect: method gets called, it asks the item if it's selected and draws appropriately.

    Therefore, I could completely remove all of the should/did/select/Deselect methods from the delegate without any problem, and it all just worked!