在SwiftUI中获取鼠标/指针位置以用于macOS应用程序的轻点手势

3

目前在SwiftUI中,我们可以通过DragGesture轻松跟踪视图内部的指针位置,因为该手势会公开起始位置和相对于给定坐标空间的平移:

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组件可能会起到作用,您有什么想法吗?


1
也许这会有用 https://dev59.com/XlMI5IYBdhLWcg3wUZyd - Andrew_STOP_RU_WAR_IN_UA
你也可以通过拖动手势获取触摸位置。 - ios coder
1个回答

2

更新 iOS 16

从 iOS 16 / macOS 13 开始,新的 onTapGesture 修改器将点击/单击的位置作为 CGPoint 在动作闭包中提供:

struct ContentView: View {
  var body: some View {
    Rectangle()
      .frame(width: 200, height: 200)
      .onTapGesture { location in 
        print("Tapped at \(location)")
      }
  }
}

原始答案

编辑

我通过尝试同时手势找到了更好的解决方案。这个解决方案与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>) { }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接