How to interactively rearrange NSCollectionView items while dragging over them?

112 Views Asked by At

Apps like Preview indicate the drop position for drag operations by interactively moving the items out of the way. Looks like this:

Preview.app drag and drop behaviour

IIRC, this is also the default behaviour of UICollectionView with all standard layouts. In contrast, NSCollectionView prefers to render an insertion indicator without repositioning the items, and I can’t quite figure out how to make it do the above. Interestingly, a screenshot in another question seems to show the exact behaviour I want (alas, the discussion over there is unrelated).

To clarify whether – and how – the delegate is set up for drag operations:

- (BOOL)collectionView:(NSCollectionView *)collectionView canDragItemsAtIndexPaths:(NSSet<NSIndexPath *> *)indexPaths withEvent:(NSEvent *)event {
    return YES;
}

- (id<NSPasteboardWriting>)collectionView:(NSCollectionView *)collectionView pasteboardWriterForItemAtIndexPath:(NSIndexPath *)indexPath {
    NSPasteboardItem *pasteboardItem = [[NSPasteboardItem alloc] init];
    [pasteboardItem setString:@"Whatever is fine" forType:NSPasteboardTypeString];
    
    return pasteboardItem;
}

- (NSDragOperation)collectionView:(NSCollectionView *)collectionView validateDrop:(id<NSDraggingInfo>)draggingInfo proposedIndexPath:(NSIndexPath * _Nonnull __autoreleasing *)proposedDropIndexPath dropOperation:(NSCollectionViewDropOperation *)proposedDropOperation {
    *proposedDropOperation = NSCollectionViewDropBefore;
    
    return NSDragOperationMove;
}

My initial understanding was that layout’s -layoutAttributesForDropTargetAtPoint:/-layoutAttributesForInterItemGapBeforeIndexPath: existed for this purpose. They don’t, as they only serve to place the indicator which does not affect the flow of items.

To be exact, setting an arbitrary rect (in a subclassed NSCollectionViewFlowLayout):

- (NSCollectionViewLayoutAttributes *)layoutAttributesForDropTargetAtPoint:(NSPoint)pointInCollectionView {
    NSCollectionViewLayoutAttributes *attributes = [[super layoutAttributesForDropTargetAtPoint:pointInCollectionView] copy];
    attributes.frame = NSMakeRect(60, 0, 200, 200);

    return attributes;
}

will render the indicator at {60, 0}, but that won’t affect the items in any way.

I could, of course, extend the layout further to make it aware of dragging sessions, but that already feels like a hack, and having to manually invalidate it during the drag (let alone doing it within -performBatchUpdates:completionHandler: to trigger an animated response) leaves no doubt about that. Alternatively, I could manipulate items’ frames directly, but that also feels like fighting the NSCollectionView rather than leveraging it.

Given how widespread the pattern is, there must be a proper way of implementing it without resorting to hacks. What am I missing?

Cheers.

0

There are 0 best solutions below