在 MacOS 上的 SwiftUI 中查找点击位置

Finding click location in SwiftUI on MacOS

我尝试使用 NSViewRepresentable 而不是 UIViewRepresentable 将我在 iOS 应用程序上使用的解决方案调整到 macOS。

下面是我的'Tappable View'。我的问题是当我尝试使用此视图时出现错误 Cannot find "TappableView" in scope.

谢谢。

(使用 Xcode 版本 12.0 beta 4)

import Foundation
import SwiftUI

struct TappableView: NSViewRepresentable {
    
    var tappedCallback: ((CGPoint, Int) -> Void)
        
    func makeNSView(context: NSViewRepresentableContext<TappableView>) -> NSView {
        let v = UIView(frame: .zero)
        let gesture = NSClickGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.tapped))
        gesture.numberOfTapsRequired = 1
        let gesture2 = NSClickGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.doubleTapped))
        gesture2.numberOfTapsRequired = 2
        gesture.require(toFail: gesture2)
        v.addGestureRecognizer(gesture)
        v.addGestureRecognizer(gesture2)
        return v
    }
    
    class Coordinator: NSObject {
        var tappedCallback: ((CGPoint, Int) -> Void)
        init(tappedCallback: @escaping ((CGPoint, Int) -> Void)) {
            self.tappedCallback = tappedCallback
        }
        @objc func tapped(gesture:NSClickGestureRecognizer) {
            let point = gesture.location(in: gesture.view)
            self.tappedCallback(point, 1)
        }
        @objc func doubleTapped(gesture:NSClickGestureRecognizer) {
            let point = gesture.location(in: gesture.view)
            self.tappedCallback(point, 2)
        }
    }
    
    func makeCoordinator() -> TappableView.Coordinator {
        return Coordinator(tappedCallback:self.tappedCallback)
    }
    
    func updateNSView(_ nsView: NSView, context: NSViewRepresentableContext<TappableView>) {
    }
        
}

这是工作变体

struct TappableView: NSViewRepresentable {

    var tappedCallback: ((CGPoint, Int) -> Void)

    func makeNSView(context: NSViewRepresentableContext<TappableView>) -> NSView {
        let v = NSView(frame: .zero)
        context.coordinator.configure(view: v)
        return v
    }

    class Coordinator: NSObject, NSGestureRecognizerDelegate {
        var tappedCallback: ((CGPoint, Int) -> Void)
        private var gesture: NSClickGestureRecognizer!
        private var gesture2: NSClickGestureRecognizer!

        init(tappedCallback: @escaping ((CGPoint, Int) -> Void)) {
            self.tappedCallback = tappedCallback
        }
        func configure(view: NSView) {
            gesture = NSClickGestureRecognizer(target: self, action: #selector(Coordinator.tapped))
            gesture.delegate = self
            gesture.numberOfClicksRequired = 1
            gesture2 = NSClickGestureRecognizer(target: self, action: #selector(Coordinator.doubleTapped))
            gesture2.delegate = self
            gesture2.numberOfClicksRequired = 2
            view.addGestureRecognizer(gesture)
            view.addGestureRecognizer(gesture2)
        }
        @objc func tapped(gesture:NSClickGestureRecognizer) {
            let point = gesture.location(in: gesture.view)
            self.tappedCallback(point, 1)
        }
        @objc func doubleTapped(gesture:NSClickGestureRecognizer) {
            let point = gesture.location(in: gesture.view)
            self.tappedCallback(point, 2)
        }

        func gestureRecognizer(_ gestureRecognizer: NSGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: NSGestureRecognizer) -> Bool {
            return gestureRecognizer === gesture && otherGestureRecognizer === gesture2
        }
    }

    func makeCoordinator() -> TappableView.Coordinator {
        return Coordinator(tappedCallback:self.tappedCallback)
    }

    func updateNSView(_ nsView: NSView, context: NSViewRepresentableContext<TappableView>) {
    }

}

更好的解决方案是使用 SwiftUI 手势组合而不是 NSViewRepresentable 解决方案,因为它可以更好地与 SwiftUI API 集成。所提出的其他解决方案的问题是,如果您想组合复杂的手势,则不能使用 .highPriorityGesture() 或任何其他 Gesture API。

这是一个有效的实现:

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) -> some Gesture {
        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)
    }
}

您可以像任何其他 SwiftUI 手势一样通过方便的 API .onClickGesture(count:perform:) 使用它。