3
votes

I'm implementing an application for macOS in which I use an NSCollectionView as a sort of timeline. For this I use a custom subclass of NSCollectionViewFlowLayout for the following reasons:

  1. I want the items to be scrollable horizontally only without wrapping to the next line
  2. Only NSCollectionViewFlowLayout seems to be capable of telling NSCollectionView to not scroll vertically (inspiration from here)

All of this works smoothly, however: I'm now trying to implement drag & drop reordering of the items. This also works, but I noticed that the inter item gap indicator (blue line) is not being displayed properly, even though I do return proper sizes. I calculate them as follows:

override func layoutAttributesForInterItemGap(before indexPath: IndexPath) -> NSCollectionViewLayoutAttributes?
{
    var result: NSCollectionViewLayoutAttributes? = nil

    // The itemLayoutAttributes dictionary is created in prepare() and contains
    // the layout attributes for every item in the collection view
    if indexPath.item < itemLayoutAttributes.count
    {
        if let itemLayout = itemLayoutAttributes[indexPath]
        {
            result = NSCollectionViewLayoutAttributes(forInterItemGapBefore: indexPath)
            result?.frame = NSRect(x: itemLayout.frame.origin.x - 4, y: itemLayout.frame.origin.y, width: 3, height: itemLayout.size.height)
        }
    }
    else
    {
        if let itemLayout = itemLayoutAttributes.reversed().first
        {
            result = NSCollectionViewLayoutAttributes(forInterItemGapBefore: indexPath)
            result?.frame = NSRect(x: itemLayout.value.frame.origin.x + itemLayout.value.frame.size.width + 4, y: itemLayout.value.frame.origin.y, width: 3, height: itemLayout.value.size.height)
        }
    }

    return result
}

Per documentation the indexPath passed to the method can be between 0 and the number of items in the collection view including if we're trying to drop after the last item. As you can see I return a rectangle for the inter item gap indicator that should be 3 pixels wide and the same height as an item is.

While the indicator is displayed in the correct x-position with the correct width, it is only 2 pixels high, no matter what height I pass in.

How do I fix this?

NB: If I temporarily change the layout to NSCollectionViewFlowLayout the indicator displays correctly.

I'm overriding the following methods/properties in NSCollectionViewFlowLayout:

  • prepare()
  • collectionViewContentSize
  • layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes]
  • layoutAttributesForItem(at indexPath: IndexPath) -> NSCollectionViewLayoutAttributes?
  • shouldInvalidateLayout(forBoundsChange newBounds: NSRect) -> Bool
  • layoutAttributesForInterItemGap(before indexPath: IndexPath) -> NSCollectionViewLayoutAttributes?
1

1 Answers

0
votes

You may also need to override layoutAttributesForDropTarget(at pointInCollectionView: NSPoint) -> NSCollectionViewLayoutAttributes.

The documentation mentions that the frame of the attributes returned by this method provides a bounding box for the gap, that will be used to position and size a drop target indicator (view).

You can set a breakpoint in your drag and drop code, after the drop indicator is drawn (e.g. collectionView(:acceptDrop:indexPath:dropOperation:) and then when the breakpoint is hit during a drag-and-drop session, run Xcode view debugger to examine the drop indicator view after it's added to the collection view. This way you can be sure the view hierarchy is correct and any issues are visually apparent.