1

I've tried to adapt a solution I've used on an iOS app to macOS using NSViewRepresentable instead of UIViewRepresentable.

Below is my 'Tappable View'. My problem is that when I try to use this view I get the error Cannot find "TappableView" in scope.

Thanks.

(using Xcode Version 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>) {
    }
        
}

santi.gs
  • 514
  • 3
  • 15

2 Answers2

2

Here is working variant

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

}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
2

A better solution is to use a combination of SwiftUI gestures instead of a NSViewRepresentable solution as it better integrates with SwiftUI API. The issue with the other solution proposed is that you cannot use .highPriorityGesture() or any other Gesture API if you want to compose complexe gestures.

Here is a working implementation:

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)
    }
}

You can use it via the convenient API .onClickGesture(count:perform:) like any other SwiftUI gesture.

Louis Lac
  • 5,298
  • 1
  • 21
  • 36