Search code examples
swiftswiftuimapboxuitapgesturerecognizeruiviewrepresentable

iOS SwiftUI: How to detect location of UITapGesture on view?


Using Mapbox, the map struct conforms to the UIViewRepresentable protocol. In my makeUIView() function, I create a tap gesture recognizer and add it to the map view.

struct Map: UIViewRepresentable {

    private let mapView: MGLMapView = MGLMapView(frame: .zero, styleURL: MGLStyle.streetsStyleURL)

    func makeUIView(context: UIViewRepresentableContext<Map>) -> MGLMapView {
        mapView.delegate = context.coordinator
        let gestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: Selector("tappedMap"))
        mapView.addGestureRecognizer(gestureRecognizer)
        return mapView
    }

    func updateUIView(_ uiView: MGLMapView, context: UIViewRepresentableContext<Map>) {
        print("Just updated the mapView")
    }

    func makeCoordinator() -> Map.Coordinator {
        Coordinator(appState: appState, self)
    }

    // other functions that make the struct have functions required by Mapbox 

    func makeCoordinator() -> Map.Coordinator {
        Coordinator(self)
    }

    final class Coordinator: NSObject, MGLMapViewDelegate {
        var control: Map

        //a bunch of delegate functions

        @objc func tappedMap(sender: UITapGestureRecognizer) {
            let locationInMap = sender.location(in: control)
            let coordinateSet = sender.convert(locationInMap, toCoordinateFrom: control)
        }
    }
}

Neither of the lines in the tappedMap function compile properly...also, when I have 'sender: UITapGestureRecognizer' in the parameters of tappedMap, I causes the application to crash when I tap the mapView--If I remove the parameter, then the function is at least called properly without crashing. Please help


Solution

  • OK, the first problem is your selector definition in the tapGesture declaration. Change it to this:

    let gestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.tappedMap(sender:)))
    

    Also, you need to manage the priority of gesture recognizers, since MGLMapView includes UIGestureRecognizer logic internally. I found a discussion of here:

    Adding your own gesture recognizer to MGLMapView will block the corresponding gesture recognizer built into MGLMapView. To avoid conflicts, define which gesture takes precedence.

    You can try this code in your project (pretty much just copied from the linked page above) EDIT: I needed to change the order of lines from my original answer:

    let gestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: Selector("tappedMap"))
    
    // HERE'S THE NEW STUFF:
    for recognizer in mapView.gestureRecognizers! where recognizer is UITapGestureRecognizer {
        gestureRecognizer.require(toFail: recognizer)
    }
    
    mapView.addGestureRecognizer(gestureRecognizer)
    
    

    EDIT: I was able to test this.

    I'm a little confused by the logic inside your tappedMap selector: maybe you meant something like this?

    @objc func tappedMap(sender: UITapGestureRecognizer) {
        let locationInMap = sender.location(in: control.mapView)
        let coordinateSet = sender.view?.convert(locationInMap, to: control.mapView)
    }