Workaround for NSTableView bugs introduced in macOS 27

MacOS 27 (or perhaps 26, I never used it) introduces two bugs in NSTableView (and delegate). It's always a pleasure when you find that your app no longer works properly.

First, when the delegate responds to tableView:pasteboardWriterForRow:, no row can be selected by click & drag (even if the method returns nil). Which mean that clicking and dragging the mouse on rows that are not draggable simply does nothing (no selection), which is a frustrating user experience, especially considering that the same action selects rows on macOS 15.

The second bug is that tableView.verticalMotionCanBeginDrag = NO is simply ignored. It doesn’t work. So, a click followed by an immediate vertical drag drags the clicked row instead of selecting rows if tableView:pasteboardWriterForRow: does not return nil. Again, it did select rows on macOS 15. That's what verticalMotionCanBeginDrag = NO is for.

To work around the first bug, I implement respondsToSelector: in the delegate to return NO when the selector is tableView:pasteboardWriterForRow: and the clicked row is not draggable. This solution is not elegant, as theclickedRow property of the tableview always returns -1 when the method is called. Which means I need to determine the clicked row by other means (using the mouse location and stuff).

(This solution should in principle allow working around the second bug, but I haven’t implemented it yet. It would require determining the delay between the click and the drag and the direction of the drag).

So my question is: does anyone see a better workaround for these bugs?

Answered by DTS Engineer in 913292022

I reproduced both issues in macOS 27.0.1 with a small test project of my own, built with Xcode 27.1. They appear in a view-based table view that doesn't override any mouse-handling methods. A vertical drag that starts on a row whose tableView:pasteboardWriterForRow: returns nil selects nothing. Setting verticalMotionCanBeginDrag to NO doesn't stop a vertical drag on a draggable row from starting a drag session. That second result doesn't match the comment in NSTableView.h:

If -verticalMotionCanBeginDrag is YES, then click + a vertical drag of the mouse will drag the clicked item(s). If NO, it will do a "drag select".

Your respondsToSelector: override worked in my tests too. A documented alternative for the first issue is to override canDragRowsWithIndexes:atPoint: in an NSTableView subclass. Returning NO for rows that aren't draggable restored drag selection over those rows. AppKit supplies the mouse-down point, so there's no need for clickedRow or your own hit testing:

- (BOOL)canDragRowsWithIndexes:(NSIndexSet *)rowIndexes atPoint:(NSPoint)mouseDownPoint {
    NSInteger row = [self rowAtPoint:mouseDownPoint];
    if (!RowIsDraggable(row)) {     // Your app's own test.
        return NO;
    }
    return [super canDragRowsWithIndexes:rowIndexes atPoint:mouseDownPoint];
}

That override doesn't affect the second issue. A table view subclass that overrides mouseDown: and calls super made both issues go away in every case I tried, and dragging still worked:

- (void)mouseDown:(NSEvent *)event {
    [super mouseDown:event];
}

Adopting gesture recognizers in your app describes the effect of an override like this:

Container views fall back to tracking-loop behavior in the same way when you override their mouse-handling methods.

The same article describes what an app loses with tracking-loop behavior, such as multi-finger gestures on a Mac with a touch display. That's relevant if you're deciding whether to adopt the override.

I suggest filing a separate bug report for each issue, each with a small sample project attached, and posting the FB numbers here. Bug Reporting: How and Why? has tips on creating a report that's easy to act on.

I reproduced both issues in macOS 27.0.1 with a small test project of my own, built with Xcode 27.1. They appear in a view-based table view that doesn't override any mouse-handling methods. A vertical drag that starts on a row whose tableView:pasteboardWriterForRow: returns nil selects nothing. Setting verticalMotionCanBeginDrag to NO doesn't stop a vertical drag on a draggable row from starting a drag session. That second result doesn't match the comment in NSTableView.h:

If -verticalMotionCanBeginDrag is YES, then click + a vertical drag of the mouse will drag the clicked item(s). If NO, it will do a "drag select".

Your respondsToSelector: override worked in my tests too. A documented alternative for the first issue is to override canDragRowsWithIndexes:atPoint: in an NSTableView subclass. Returning NO for rows that aren't draggable restored drag selection over those rows. AppKit supplies the mouse-down point, so there's no need for clickedRow or your own hit testing:

- (BOOL)canDragRowsWithIndexes:(NSIndexSet *)rowIndexes atPoint:(NSPoint)mouseDownPoint {
    NSInteger row = [self rowAtPoint:mouseDownPoint];
    if (!RowIsDraggable(row)) {     // Your app's own test.
        return NO;
    }
    return [super canDragRowsWithIndexes:rowIndexes atPoint:mouseDownPoint];
}

That override doesn't affect the second issue. A table view subclass that overrides mouseDown: and calls super made both issues go away in every case I tried, and dragging still worked:

- (void)mouseDown:(NSEvent *)event {
    [super mouseDown:event];
}

Adopting gesture recognizers in your app describes the effect of an override like this:

Container views fall back to tracking-loop behavior in the same way when you override their mouse-handling methods.

The same article describes what an app loses with tracking-loop behavior, such as multi-finger gestures on a Mac with a touch display. That's relevant if you're deciding whether to adopt the override.

I suggest filing a separate bug report for each issue, each with a small sample project attached, and posting the FB numbers here. Bug Reporting: How and Why? has tips on creating a report that's easy to act on.

Written by DTS Engineer in 913292022

A table view subclass that overrides mouseDown: and calls super made both issues go away in every case I tried, and dragging still worked:

Wow. Would would have thought that overriding a method and only calling super would fix any bug? Thanks anyway. Though I would need to create subclasses for table views and outline views just to work around issues.
I had already filed separate bug reports: FB25342199 and FB25342038