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

Thanks for posting FB25342199 and FB25342038.

It's a surprising result. In most code, an override that only calls super changes nothing. In macOS 27, though, AppKit sends clicks and drags in controls and container views through gesture recognizers. It falls back to the older tracking-loop handling in certain cases, including a subclass that overrides a mouse-handling method. Adopting gesture recognizers in your app says AppKit treats the override "as a replacement for its own click handling, rather than as a side effect." It continues:

AppKit decides based on whether your subclass overrides the method, not on what the override does, so the fallback still applies if your override calls super.mouseDown(with:). Overriding mouseDragged(with:) or mouseUp(with:) has the same effect.

The article says container views fall back in the same way. In my tests, both issues appeared only on the gesture-recognizer path. With the override, the table view used tracking-loop handling, where drag selection worked and verticalMotionCanBeginDrag took effect. The table view's gestureRecognizers property shows which path it's on. That property held seven recognizers without the override and none with it. I tested only NSTableView, not NSOutlineView.

Please continue to test your software in release and pre-release versions of system software made available to you through your developer.apple.com account. When you do, add details about that into your bug report to keep it up to date. Please see developer.apple.com/download.