Changing mouse cursor with NSCursor.push() or .set() is soon replaced by arrow cursor again

I noticed an issue in macOS 14 which I didn't have on macOS 13. I used to be able to set a custom mouse cursor when it moves over a certain view area in my app, but now it's regularly reset to the standard arrow cursor.

This is easily reproduced with the following code. When I move the mouse in and out of the red rectangle. When moving in, the cursor should become a hand, and when moving out an arrow again. It seems that particularly when moving the mouse to the right of the red rectangle it quickly gets reset to the arrow cursor, while moving the mouse on the left side it often stays a hand.

Even uncommenting the line with cursor?.set() makes the mouse cursor flicker between arrow and hand.

Is this a known bug or am I doing something wrong?

class ViewController: NSViewController {
    
    var cursor: NSCursor?
    let subframe = CGRect(x: 100, y: 100, width: 300, height: 100)

    override func loadView() {
        let subview = NSView(frame: subframe)
        subview.wantsLayer = true
        subview.layer!.backgroundColor = NSColor.red.cgColor
        view = NSView(frame: CGRect(x: 0, y: 0, width: 500, height: 300))
        view.addSubview(subview)
        view.addTrackingArea(NSTrackingArea(rect: .zero, options: [.activeInKeyWindow, .inVisibleRect, .cursorUpdate, .mouseMoved], owner: self))
    }
    
    override func mouseMoved(with event: NSEvent) {
        if subframe.contains(view.convert(event.locationInWindow, from: nil)) {
            if cursor == nil {
                cursor = .openHand
                cursor!.push()
                print("set cursor")
            }
        } else if let cursor = cursor {
            cursor.pop()
            self.cursor = nil
            print("unset cursor")
        }
//        cursor?.set()
    }
    
}

The intended function to use when you specify the .cursorUpdate option on your tracking area is cursorUpdate(with:):

https://developer.apple.com/documentation/appkit/nsresponder/1525066-cursorupdate/

In the past, you could sometimes get away with setting the cursor only in functions like mouseMoved(with:), but they weren't the intended solution. I don't have any insight into the details, but it looks like Sonoma perhaps does things in a slightly different order, producing the results you've seen. I recommend you modify your code to use cursorUpdate(with:) and see if that restores the intended results.

Note: It was always true that there might be — depending on the details of how your tracking area is configured and used — some edge cases where you'd also need to set the cursor in functions other than cursorUpdate(with:), and that might still be necessary.

The intended function to use when you specify the .cursorUpdate option on your tracking area is cursorUpdate(with:):

Thanks, that really seems to solve the issue with the sample code I provided above. I guess updating the cursor once in cursorUpdate(with:) makes macOS aware that there is a custom cursor so it doesn't try to reset it continuously, as opposed to "force" setting it in mouseMoved(with:).

The problem now is that I was really looking for a way to set a custom cursor whenever the mouse moves, because in my custom view there can be many hotspots that should change the mouse cursor, and I guess adding hundreds of tracking areas is not ideal... or is it? These hotspots also change very frequently. It seems like calculating and creating all individual tracking areas is very inefficient, as opposed to dynamically finding out what the cursor should be depending on the mouse cursor's position (particularly since in my case this can be done with some simple calculations).

Changing mouse cursor with NSCursor.push() or .set() is soon replaced by arrow cursor again
 
 
Q