How to get mouse location with SwiftUI?
With help from SwiftUILab@twitter
struct ContentView: View {
@State private var pt: CGPoint = .zero
var body: some View {
let myGesture = DragGesture(minimumDistance: 0, coordinateSpace: .global).onEnded({
self.pt = $0.startLocation
})
// Spacers needed to make the VStack occupy the whole screen
return VStack {
Spacer()
HStack {
Spacer()
Text("Tapped at: \(pt.x), \(pt.y)")
Spacer()
}
Spacer()
}
.border(Color.green)
.contentShape(Rectangle()) // Make the entire VStack tappabable, otherwise, only the areay with text generates a gesture
.gesture(myGesture) // Add the gesture to the Vstack
}
}
Get Coordinates of Moving Pointer in SwiftUI (iPadOS)
The WWDC video covers this very topic:
Handle trackpad and mouse input
Add SupportsIndirectInputEvents
to your Info.plist
From the video:
It is required in order to get the new touch type indirect pointer and
EventType.transform.
Existing projects do not have this key set and will need to add it. Starting with iOS 14 and macOS Big Sur SDKs, new UIKit and SwiftUI
projects will have this value set to "true."
In addition you will use UIPointerInteraction
. This tutorial shows you step by step including custom cursors:
https://pspdfkit.com/blog/2020/supporting-pointer-interactions/
Get mouse/pointer position for tap gestures in SwiftUI for macOS apps
Update iOS 16
Starting form iOS 16 / macOS 13, the new onTapGesture
modifier makes available the location of the tap/click in the action closure as a CGPoint
:
struct ContentView: View {
var body: some View {
Rectangle()
.frame(width: 200, height: 200)
.onTapGesture { location in
print("Tapped at \(location)")
}
}
}
Original Answer
Edit
I found a better solution by experimenting with simultaneous gestures. This solution integrated perfectly with SwiftUI gestures and works similarly to other existing gestures:
import SwiftUI
struct ClickGesture: Gesture {
let count: Int
let coordinateSpace: CoordinateSpace
typealias Value = SimultaneousGesture<TapGesture, DragGesture>.Value
init(count: Int = 1, coordinateSpace: CoordinateSpace = .local) {
precondition(count > 0, "Count must be greater than or equal to 1.")
self.count = count
self.coordinateSpace = coordinateSpace
}
var body: SimultaneousGesture<TapGesture, DragGesture> {
SimultaneousGesture(
TapGesture(count: count),
DragGesture(minimumDistance: 0, coordinateSpace: coordinateSpace)
)
}
func onEnded(perform action: @escaping (CGPoint) -> Void) -> _EndedGesture<ClickGesture> {
ClickGesture(count: count, coordinateSpace: coordinateSpace)
.onEnded { (value: Value) -> Void in
guard value.first != nil else { return }
guard let location = value.second?.startLocation else { return }
guard let endLocation = value.second?.location else { return }
guard ((location.x-1)...(location.x+1)).contains(endLocation.x),
((location.y-1)...(location.y+1)).contains(endLocation.y) else {
return
}
action(location)
}
}
}
extension View {
func onClickGesture(
count: Int,
coordinateSpace: CoordinateSpace = .local,
perform action: @escaping (CGPoint) -> Void
) -> some View {
gesture(ClickGesture(count: count, coordinateSpace: coordinateSpace)
.onEnded(perform: action)
)
}
func onClickGesture(
count: Int,
perform action: @escaping (CGPoint) -> Void
) -> some View {
onClickGesture(count: count, coordinateSpace: .local, perform: action)
}
func onClickGesture(
perform action: @escaping (CGPoint) -> Void
) -> some View {
onClickGesture(count: 1, coordinateSpace: .local, perform: action)
}
}
Previous Workaround
Based on this SO question linked in a comment, I adapted the UIKit solution suitable for macOS.
Apart from changing the types I added a computation to compute the position in the macOS coordinate system style. There is still an ugly force unwrap but I do not know enough AppKit to remove it safely.
You should use this modifier before other gestures modifiers if you want multiple simultaneous gestures on one view.
Currently the CoordinateSpace
argument only works for .local
and .global
but is not implemented correctly for .named(Hashable)
.
If you have solutions for some of those issues I'll update the answer.
Here is the code:
public extension View {
func onTapWithLocation(count: Int = 1, coordinateSpace: CoordinateSpace = .local, _ tapHandler: @escaping (CGPoint) -> Void) -> some View {
modifier(TapLocationViewModifier(tapHandler: tapHandler, coordinateSpace: coordinateSpace, count: count))
}
}
fileprivate struct TapLocationViewModifier: ViewModifier {
let tapHandler: (CGPoint) -> Void
let coordinateSpace: CoordinateSpace
let count: Int
func body(content: Content) -> some View {
content.overlay(
TapLocationBackground(tapHandler: tapHandler, coordinateSpace: coordinateSpace, numberOfClicks: count)
)
}
}
fileprivate struct TapLocationBackground: NSViewRepresentable {
let tapHandler: (CGPoint) -> Void
let coordinateSpace: CoordinateSpace
let numberOfClicks: Int
func makeNSView(context: NSViewRepresentableContext<TapLocationBackground>) -> NSView {
let v = NSView(frame: .zero)
let gesture = NSClickGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.tapped))
gesture.numberOfClicksRequired = numberOfClicks
v.addGestureRecognizer(gesture)
return v
}
final class Coordinator: NSObject {
let tapHandler: (CGPoint) -> Void
let coordinateSpace: CoordinateSpace
init(handler: @escaping ((CGPoint) -> Void), coordinateSpace: CoordinateSpace) {
self.tapHandler = handler
self.coordinateSpace = coordinateSpace
}
@objc func tapped(gesture: NSClickGestureRecognizer) {
let height = gesture.view!.bounds.height
var point = coordinateSpace == .local
? gesture.location(in: gesture.view)
: gesture.location(in: nil)
point.y = height - point.y
tapHandler(point)
}
}
func makeCoordinator() -> TapLocationBackground.Coordinator {
Coordinator(handler: tapHandler, coordinateSpace: coordinateSpace)
}
func updateNSView(_: NSView, context _: NSViewRepresentableContext<TapLocationBackground>) { }
}
Getting Mouse Coordinates in Swift
You should take a look at NSEvent method mouseLocation
edit/update: Xcode 11 • Swift 5.1
If you would like to monitor events on any window when your app is active, you can add a LocalMonitorForEvents matching mouseMoved mask and if it is not active a GlobalMonitorForEvents. Note that you need set to your window property acceptsMouseMovedEvents
to true
import Cocoa
class ViewController: NSViewController {
lazy var window: NSWindow = self.view.window!
var mouseLocation: NSPoint { NSEvent.mouseLocation }
var location: NSPoint { window.mouseLocationOutsideOfEventStream }
override func viewDidLoad() {
super.viewDidLoad()
NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
print("mouseLocation:", String(format: "%.1f, %.1f", self.mouseLocation.x, self.mouseLocation.y))
print("windowLocation:", String(format: "%.1f, %.1f", self.location.x, self.location.y))
return $0
}
NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved]) { _ in
print(String(format: "%.0f, %.0f", self.mouseLocation.x, self.mouseLocation.y))
}
}
override func viewWillAppear() {
super.viewWillAppear()
window.acceptsMouseMovedEvents = true
}
}
Sample project
How to get location of a view- swiftUI
Wrap your view with a GeometryReader
:
struct ContentView: View {
var body: some View {
GeometryReader { geo in
// frame of YourView in global coordinates
// gives you minX, minY, widht, height and more
_ = geo.frame(in: .global)
YourView()
.gesture(DragGesture()
.onChanged({ value in
_ = value.translation.width // relative drag x
_ = value.translation.height // relative drag y
})
)
}
}
}
Related Topics
How to Create a Hotspot Network in iOS App Using Swift
Expression Resolves to an Unused Function
Swift Struct with Lazy, Private Property Conforming to Protocol
Swift - Getting Only Alphanumeric Characters from String
How to Cast [Int8] to [Uint8] in Swift
Classes in Swift Files Inside Folder References Not Seen by Xcode 10's Compiler
Swift: Specialize Method of Generic Class for Function Types
How to Set an Initial Value for @Nsmanaged Property Pfobject Subclass
Trying to Add a Protocol to a Class Signature in Swift
Swift:Pause and Resume Nstimer
Partial Application of 'Mutating' Method Is Not Allowed
Using Uilexicon to Implement Autocorrect in iOS 8 Keyboard Extension
How to Open Your App's Settings (Inside the Settings App) with Swift (iOS 11)
Swift: Reflecting Properties of Subclass of Nsmanagedobject
Drag and Drop Image from Uicollectionview to Uiview
How to Print Escape Sequence Characters in Swift
Is It Safe to Force Unwrap an Optional After Checking It Is Not Nil