'SwiftUI System Cursor
I'm trying to change the cursor to a crosshair in SwiftUI on MacOS.
I've put the following code into the AppDelegate
applicationDidFinishLaunching()
function:
NSCursor.crosshair.set()
When the application loads, I see the cursor change to the crossHair, and then swap straight back to the standard pointer.
It would be great to hear what I'm doing wrong here.
Thanks all.
Solution 1:[1]
It is not the way how NSCursor
works. There is a stack of cursors so any standard (or non-standard) control can push own type of cursor on top and make that cursor current. So you just place first cursor, but then some standard view having own default cursor replaces it.
As SwiftUI does not allow now to manage cursors natively, the solution might be
a) either to set/push desired cursor on SwiftUI view appear/disappear, or
b) add cursor rect to NSHostingController
view using standard NSView.addCursorRect
method.
Update: some quick demo from existing project (part of custom button solution)
struct DemoCustomCursor: View {
var body: some View {
Button(action: {}) {
Text("Cross Button")
.padding(20)
.background(Color.blue)
}.buttonStyle(PlainButtonStyle())
.onHover { inside in
if inside {
NSCursor.crosshair.push()
} else {
NSCursor.pop()
}
}
}
}
Solution 2:[2]
Here is Swift Playground code to illustrate the problem:
import Foundation
import SwiftUI
import PlaygroundSupport
PlaygroundPage.current.setLiveView(
SplitView().frame(width: 600, height:600).border(Color.black)
)
struct SplitView: View {
@State var position: CGFloat = 10.0
var body: some View {
Text("top")
.frame(height: position)
PaneDivider(position: $position)
Text("bottom")
Spacer()
}
}
struct PaneDivider: View {
@Binding var position: CGFloat
@GestureState private var isDragging = false // Will reset to false when dragging has ended
var body: some View {
Rectangle().frame(height:10).foregroundColor(Color.gray)
.onHover { inside in
if !isDragging {
if inside { NSCursor.resizeUpDown.push() }
else { NSCursor.pop() }
}
}
.gesture(DragGesture()
.onChanged { position += $0.translation.height }
.updating($isDragging) { (value, state, transaction) in
if !state { NSCursor.resizeUpDown.push() } // This is overridden, something else in the system is pushing the arrow cursor during the drag
state = true
}
.onEnded { _ in NSCursor.pop() })
}
}
Setting a breakpoint on -[NSCursor set] shows it firing constantly during the drag in
Setting a breakpoint on -[NSCursor set] shows it firing constantly during the drag in
* frame #0: 0x00000001a48c2944 AppKit`-[NSCursor set]
frame #1: 0x00000001a491cc4c AppKit`forwardMethod + 200
frame #2: 0x00000001a491cc4c AppKit`forwardMethod + 200
frame #3: 0x00000001a4924428 AppKit`-[NSView cursorUpdate:] + 132
frame #4: 0x00000001a48cb2e4 AppKit`-[NSWindow(NSCursorRects) _setCursorForMouseLocation:] + 432
frame #5: 0x00000001a47f0d70 AppKit`_NSWindowDisplayCycleUpdateStructuralRegions + 544
frame #6: 0x00000001a47ec028 AppKit`__NSWindowGetDisplayCycleObserverForUpdateStructuralRegions_block_invoke + 428
frame #7: 0x00000001a47e59ec AppKit`NSDisplayCycleObserverInvoke + 188
frame #8: 0x00000001a47e5568 AppKit`NSDisplayCycleFlush + 832
frame #9: 0x00000001a81a8544 QuartzCore`CA::Transaction::run_commit_handlers(CATransactionPhase) + 120
frame #10: 0x00000001a81a754c QuartzCore`CA::Transaction::commit() + 336
frame #11: 0x00000001a488e6e0 AppKit`__62+[CATransaction(NSCATransaction) NS_setFlushesWithDisplayLink]_block_invoke + 304
frame #12: 0x00000001a4fe2a10 AppKit`___NSRunLoopObserverCreateWithHandler_block_invoke + 64
frame #13: 0x00000001a1f28e14 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 36
frame #14: 0x00000001a1f28c60 CoreFoundation`__CFRunLoopDoObservers + 572
frame #15: 0x00000001a1f281a8 CoreFoundation`__CFRunLoopRun + 764
frame #16: 0x00000001a1f27734 CoreFoundation`CFRunLoopRunSpecific + 600
frame #17: 0x00000001a9e25b84 HIToolbox`RunCurrentEventLoopInMode + 292
frame #18: 0x00000001a9e258f8 HIToolbox`ReceiveNextEventCommon + 552
frame #19: 0x00000001a9e256b8 HIToolbox`_BlockUntilNextEventMatchingListInModeWithFilter + 72
frame #20: 0x00000001a47114ec AppKit`_DPSNextEvent + 836
frame #21: 0x00000001a470fe8c AppKit`-[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 1292
frame #22: 0x00000001a4701d18 AppKit`-[NSApplication run] + 596
frame #23: 0x00000001a46d3728 AppKit`NSApplicationMain + 1064
frame #24: 0x00000001c25f11b4 SwiftUI`generic specialization <SwiftUI.TestingAppDelegate> of function signature specialization <Arg[0] = Existential To Protocol Constrained Generic> of SwiftUI.runApp(__C.NSApplicationDelegate) -> Swift.Never + 96
frame #25: 0x00000001c2e34ba0 SwiftUI`SwiftUI.runApp<?_0_0 where ?_0_0: SwiftUI.App>(?_0_0) -> Swift.Never + 220
frame #26: 0x00000001c29de854 SwiftUI`static SwiftUI.App.main() -> () + 128
frame #27: 0x0000000100c8c540 Calculator`static CalculatorApp.$main(self=Calculator.CalculatorApp) at CalculatorApp.swift:5:1
frame #28: 0x0000000100c8c5e0 Calculator`main at CalculatorApp.swift:0
frame #29: 0x00000001a1e48420 libdyld.dylib`start + 4
Looks like the same pre-SwiftUI problem as http://cocoadev.github.io/CursorFlashingProblems/
Adding NSApp.windows[0].disableCursorRects()
at the start of the DragGesture and NSApp.windows[0].enableCursorRects()
in .onEnded
fixes the problem for me here.
Except NSApp.windows[0]
isn't always the front window. Darn. NSApp.windows.forEach { $0.disableCursorRects() }
works though. ;)
Solution 3:[3]
onHover
doesn't exist in macCatalyst, but you can add it yourself. The alternative solution is explained https://stackoverflow.com/a/60748101/1450348
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | |
Solution 3 | manman |