NSTableView dragging source image

I have a simple problem, but I’m not able to solve it in an easy way, so I suspect I’m doing something wrong.

I have a simple view-based NSTableView, which is a dragging source. The data being dragged are provided to the pasteboard using the standard data source method - [NSObject tableView:writeRowsWithIndexes:toPasteboard:]. When rows of the table view are dragged, AppKit automatically creates a dragging image consisting of the visual copy for all dragged rows and all columns of the table view. If I want only particular columns, I can override -[NSTableView dragImageForRowsWithIndexes:tableColumns:event:offset:] and include only the columns I want in the dragged image.

Since -[NSObject tableView:writeRowsWithIndexes:toPasteboard:]is rendered deprecated as of macOS 11, I want to use the alternative (which also supports multiple item dragging) -[NSObject tableView:pasteboardWriterForRow:]. However, when I use this method, AppKit creates a dragging image consisting of the visual copy for all dragged rows, BUT ONLY the column where the drag started. Overriding -[NSTableView dragImageForRowsWithIndexes:tableColumns:event:offset:] has no effect, as in this case that method isn’t being called at all.

So the question is, how do I influence the dragging image creation in the latter case? I haven’t found in docs anything related to that (except for the method mentioned above, but not being called in this case). Implementing data source method -[NSObject tableView:updateDraggingItemsForDrag:] doesn’t help, as it’s really intended to change the dragging image after the dragging has started and at the time it’s called for the first time, the AppKit created image is already there and visible. I know I can (try to) subclass NSTableView and override NSDraggingSource methods (and call super at the end of each override to make sure the data source object is being messaged as well) but I hope there's a simpler and shorter solution.

Thanks a lot for any solution, hint or a suggestion.

-- Dragan

Replies

Hi Dragan,

I was faced with the same issue, and came across your posting while searching for an approach. I want to drag whole-row images regardless of which column was targeted by the mouse.

Building on techniques described at https://www.mail-archive.com/cocoa-dev%40lists.apple.com/msg108722.html (for some reason this web app won't let me format that as a link) and here, I came up with this:

func tableView(_ tableView: NSTableView,
			   draggingSession session: NSDraggingSession,
			   willBeginAt screenPoint: NSPoint,
			   forRowIndexes rowIndexes: IndexSet) {
	session.enumerateDraggingItems(options: .concurrent,
								   for: nil,
								   classes: [NSPasteboardItem.self],
								   searchOptions: [:]) { (draggingItem, index, stop) in
		// Get the row index for this drag item. Could cheat and map `index` to `rowIndexes`, but this is cleaner.
		guard let pasteboardItem = draggingItem.item as? NSPasteboardItem,
			  let rowIndex = pasteboardItem.propertyList(forType: "com.example.RowIndexPasteboardType") as? Int else {
			stop.pointee = true
			return
		}
		
		// Compensate for the dragged column offset.
		let dragPointInWindow = tableView.window!.convertPoint(fromScreen: screenPoint)
		let dragPointInView = tableView.convert(dragPointInWindow, from: nil)
		let columnIndex = tableView.column(at: dragPointInView)
		let columnRect = tableView.rect(ofColumn: columnIndex)
		draggingItem.draggingFrame.origin.x -= columnRect.origin.x + 7 // The 7px error is a mystery to me (on 12.6 at least).
		
		// Register a provider that will render a bitmap image of the whole row.
		draggingItem.imageComponentsProvider = {
			guard let rowView = tableView.rowView(atRow: rowIndex, makeIfNecessary: false),
				  let imageRep = rowView.bitmapImageRepForCachingDisplay(in: rowView.bounds) else {
				return []
			}
			imageRep.size = rowView.bounds.size
			rowView.cacheDisplay(in: rowView.bounds, to: imageRep)
			let image = NSImage(size: rowView.bounds.size)
			image.addRepresentation(imageRep)
			
			let component = NSDraggingImageComponent(key: .icon)
			component.contents = image
			component.frame.size = image.size
			return [component]
		}
	}
}

It seems to work well enough, and is implemented in the data source (no subclassing required). How's it grab ya?