NSCursor reset to arrow in macOS SwiftUI app

In trying to implement a custom variation of HSplitView/VSplitView for macOS, I found it impossible to achieve correct behavior of the NSCursor while dragging the split view divider. Something else is resetting NSCursor to an arrow cursor when a SwiftUI view updates.

Moving the mouse over the divider View changes the NSCursor to a resize cursor. However, when the insertion is blinking, after 1/2 a second, on the blink the cursor changes back to an arrow.

Turning off "blink", then hovering over the divider, the NSCursor stays as a resize cursor. However, as soon as you click and drag to move the divider, again it changes back to an arrow cursor, flashing between the two NSCursor as you move the divider.

It seems like something in SwiftUI is doing this, but I can't tell.

Is it possible to implement a custom VSplitView within SwiftUI and achieve consistent behavior of the NSCursor?

Full code below

import AppKit
import SwiftUI

@main
struct NSCursorBugApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

let blinkTimer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()

struct ContentView: View {
    @State var position = 20.0
    @State var opacity = 1.0
    @State var blink = true
    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            Color.gray
                .frame(height: position - 4)
                .overlay{
                    Rectangle().frame(width: 1, height: 20).opacity(opacity).padding(.trailing, 200).padding(20)
                }
            PaneDivider(position: $position)
            Color.cyan
            Toggle("Blink", isOn: $blink)
        }
        .onReceive(blinkTimer) {_ in if blink { opacity = 1 - opacity } }
        .frame(width: 500, height: 500)
        .coordinateSpace(name: "stack")
    }
}

struct PaneDivider: View {
    @Binding var position: Double

    let dividerSize = 8.0
    let circleSize  = 6.0

    var body: some View {
        Group {
            Divider()
            ZStack {
                Rectangle().fill(Color(white: 0.99))
                Circle().frame(width: circleSize, height: circleSize).foregroundColor(Color(white: 0.89))
            }.frame(height: 8)
            Divider()
        }
        #if os(macOS)
        .background(NSCursorView())
        #endif
        .gesture(DragGesture(minimumDistance: 1, coordinateSpace: .named("stack"))
        .onChanged { position = max(0, $0.location.y) })
    }
}

/// Helper NSView as .background to SwiftUI PaneDivider View to set resize NSCursor when inside view bounds

class NSCursorPlatformView : NSView {
    var horizontal = true
    var trackingArea: NSTrackingArea? = nil

    func clearTrackingAreas() {
        if let trackingArea = trackingArea { removeTrackingArea(trackingArea) }
        trackingArea = nil
    }

    override func updateTrackingAreas() {
        clearTrackingAreas()
        let area = NSTrackingArea(rect: bounds, options: [.mouseEnteredAndExited, .mouseMoved, .activeInKeyWindow], owner: self, userInfo: nil)
        addTrackingArea(area)
        trackingArea = area
    }

    override func viewWillMove(toWindow newWindow: NSWindow?) {
        if newWindow == nil { clearTrackingAreas() }
        else { updateTrackingAreas() }
    }

    override func mouseEntered(with event: NSEvent) { updateCursor(with: event) }
    override func mouseExited(with event: NSEvent)  { updateCursor(with: event) }
    override func mouseMoved(with event: NSEvent)   { updateCursor(with: event) }
    override func cursorUpdate(with event: NSEvent) {} // to stop system from resetting cursor after us

    func updateCursor(with event: NSEvent) {
        let p = convert(event.locationInWindow, from: nil)
        if bounds.contains(p) {
            (horizontal ? NSCursor.resizeUpDown : NSCursor.resizeLeftRight).set()
        } else {
            NSCursor.arrow.set()
        }
    }
}

struct NSCursorView: NSViewRepresentable {
    var horizontal = true
    func makeNSView(context: Context) -> NSCursorPlatformView {
        let view = NSCursorPlatformView()
        view.horizontal = horizontal
        return view
        }
    func updateNSView(_ nsView: NSCursorPlatformView, context: Context) { }
}

Maybe, this reply will help you: https://stackoverflow.com/a/67851290/7964697

PS:

That is how I use this solution in my app...

.onHover { isHovered in
    self.imageHovered = isHovered
    DispatchQueue.main.async {
        if self.imageHovered {
            // Looks like ugly hack, but otherwise cursor gets reset to standard arrow. 
            // See https://stackoverflow.com/a/62984079/7964697 for details.
            NSApp.windows.forEach { $0.disableCursorRects() } 
        
            // swiftlint:disable:next force_unwrapping
            NSCursor(image: NSImage(named: "ZoomPlus")!, hotSpot: NSPoint(x: 9, y: 9)).push() // Cannot be nil.
        } else {
            NSCursor.pop()
            NSApp.windows.forEach { $0.enableCursorRects() }
        }
    } 
}
NSCursor reset to arrow in macOS SwiftUI app
 
 
Q