2

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.

K Lee
  • 179
  • 1
  • 8
  • The documentation of `NSCollectionView` says "The collection view delegate makes decisions about behaviors. The delegate also coordinates the dragging and dropping of items.". Have you tried using any delegate methods? – Willeke Apr 24 '23 at 18:39
  • Of course. Drag and drop in collections wouldn’t even work without the delegate. I’m omitting it here merely because, even in their minimal form, collection delegates tend to be uncomfortably verbose – and people increasingly Objective-C averse; but I do register pasteboard types and return `YES` in enough places to see the insertion indicator I’m describing. Do you think I should include the delegate code after all? – K Lee Apr 24 '23 at 19:16
  • Is `collectionView(_:validateDrop:proposedIndexPath:dropOperation:)` implemented? Do you change `proposedDropOperation`? – Willeke Apr 24 '23 at 21:09
  • Yes. I’ve updated the question to include the relevant parts of the implementation. – K Lee Apr 25 '23 at 06:56
  • I don't think a drop insertion gap is supported. `[collectionView setValue:@YES forKey:@"opensDropInsertionGaps"]` does the trick but it's private API and it's a bit buggy. – Willeke Apr 26 '23 at 22:03
  • Hah, that would explain why it’s seen extensively throughout macOS but not so much in third-party apps. I think you’re right, that private property wouldn’t be there in the first place if the functionality was more readily available (I should’ve thought about reflection; knew I was missing something obvious). Thanks a lot for looking into this. Do you feel like posting this as an answer so I can accept it? – K Lee Apr 27 '23 at 10:52

0 Answers0