为 macOS 应用程序的 SwiftUI 中的点击手势获取 mouse/pointer 个位置
Get mouse/pointer position for tap gestures in SwiftUI for macOS apps
目前,由于 SwiftUI 中的 DragGesture
,我们可以在拖动手势期间轻松跟踪视图内的指针位置,因为此手势公开了起始位置和相对于给定坐标 space 的平移:
struct Example: View {
@GestureState private var startPosition: CGPoint = .zero
@GestureState private var translation: CGSize = .zero
var body: some View {
Rectangle()
.fill(Color.black)
.frame(width: 600, height: 400)
.coordinateSpace(name: "rectangle")
.overlay(coordinates)
.gesture(dragGesture)
}
var dragGesture: some Gesture {
DragGesture(minimumDistance: 5, coordinateSpace: .named("rectangle"))
.updating($startPosition) { (value, state, _) in
state = value.startLocation
}
.updating($translation) { (value, state, _) in
state = value.translation
}
}
var coordinates: some View {
Text("\(startPosition.x) \(startPosition.y) \(translation.width) \(translation.height)")
.foregroundColor(.white)
.font(Font.body.monospacedDigit())
}
}
相反,TapGesture
不提供任何有关点击位置的信息,它仅在点击发生时发出信号:
var tapGesture: some Gesture {
TapGesture()
.onEnded { _ in
print("A tap occurred")
}
}
有没有一种简单的方法可以在点击手势发生时获取指针位置?
我在网上发现了很少的解决方案,但它们通常需要大量样板文件或者不方便使用。
一些 UIKit 的包装器认为可以做到这一点,有什么想法吗?
编辑
我通过尝试同步手势找到了更好的解决方案。该解决方案与 SwiftUI 手势完美集成,并且与其他现有手势的工作方式类似:
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)
}
}
以前的解决方法
基于this评论中链接的SO问题,我改编了适用于macOS的UIKit方案。
除了更改类型外,我还添加了一个计算来计算 macOS 坐标系样式中的位置。仍然有一个丑陋的力量解开,但我不知道足够的 AppKit 来安全地删除它。
如果您希望在一个视图上同时使用多个手势,则应在使用其他手势修饰符之前使用此修饰符。
目前 CoordinateSpace
参数仅适用于 .local
和 .global
但未正确实现 .named(Hashable)
.
如果您对其中一些问题有解决方案,我会更新答案。
代码如下:
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>) { }
}
目前,由于 SwiftUI 中的 DragGesture
,我们可以在拖动手势期间轻松跟踪视图内的指针位置,因为此手势公开了起始位置和相对于给定坐标 space 的平移:
struct Example: View {
@GestureState private var startPosition: CGPoint = .zero
@GestureState private var translation: CGSize = .zero
var body: some View {
Rectangle()
.fill(Color.black)
.frame(width: 600, height: 400)
.coordinateSpace(name: "rectangle")
.overlay(coordinates)
.gesture(dragGesture)
}
var dragGesture: some Gesture {
DragGesture(minimumDistance: 5, coordinateSpace: .named("rectangle"))
.updating($startPosition) { (value, state, _) in
state = value.startLocation
}
.updating($translation) { (value, state, _) in
state = value.translation
}
}
var coordinates: some View {
Text("\(startPosition.x) \(startPosition.y) \(translation.width) \(translation.height)")
.foregroundColor(.white)
.font(Font.body.monospacedDigit())
}
}
相反,TapGesture
不提供任何有关点击位置的信息,它仅在点击发生时发出信号:
var tapGesture: some Gesture {
TapGesture()
.onEnded { _ in
print("A tap occurred")
}
}
有没有一种简单的方法可以在点击手势发生时获取指针位置?
我在网上发现了很少的解决方案,但它们通常需要大量样板文件或者不方便使用。
一些 UIKit 的包装器认为可以做到这一点,有什么想法吗?
编辑
我通过尝试同步手势找到了更好的解决方案。该解决方案与 SwiftUI 手势完美集成,并且与其他现有手势的工作方式类似:
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)
}
}
以前的解决方法
基于this评论中链接的SO问题,我改编了适用于macOS的UIKit方案。
除了更改类型外,我还添加了一个计算来计算 macOS 坐标系样式中的位置。仍然有一个丑陋的力量解开,但我不知道足够的 AppKit 来安全地删除它。
如果您希望在一个视图上同时使用多个手势,则应在使用其他手势修饰符之前使用此修饰符。
目前 CoordinateSpace
参数仅适用于 .local
和 .global
但未正确实现 .named(Hashable)
.
如果您对其中一些问题有解决方案,我会更新答案。
代码如下:
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>) { }
}