macosswiftuinsviewrepresentable

Finding click location in SwiftUI on MacOS


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


Solution

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