How to Get Mouse Location with Swiftui

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



Leave a reply



Submit