mapbox / mapbox-maps-ios

Interactive, thoroughly customizable maps for iOS powered by vector tiles and Metal
https://www.mapbox.com/mapbox-mobile-sdk
Other
481 stars 157 forks source link

Draggable annotations missing in v10 SDK #829

Closed samcrawford closed 2 years ago

samcrawford commented 3 years ago

Pre-v10, Mapbox provided support for draggable annotations, and documented a nice example at https://web.archive.org/web/20210124190424/https://docs.mapbox.com/ios/maps/examples/draggable-views/ (using an archive.org link because the mapbox.com link no longer exists).

In the v10 iOS SDK, the migration guide says:

Drag and drop support is not supported. However, this can be implemented with an advanced usage of various style APIs to update the data source of a GeoJSONSource after responding to a UIGestureRecognizer.

Can Mapbox provide an example, or at least some more detailed hints as to how we can get back draggable annotations?

I'd be happy to contribute a worked example if I can get some hints as to what the "advanced usage of various style APIs" might be!

samcrawford commented 3 years ago

I've found a solution. I can now drag point annotations, and I haven't needed to modify the core MapboxMaps project to do so. For the benefit of others, I've pasted an excerpt of my code below. This was achieved using Mapbox Maps for iOS 10.0.0.

My code is inspired by https://github.com/mapbox/mapbox-maps-ios/issues/501#issuecomment-884744996

Note to Mapbox people: Feel free to use this example if you wish. I hope you can improve the examples in your documentation (especially for things like this that existed in the previous SDK version), this was quite painful to figure out.

class MapViewController: UIViewController, AnnotationInteractionDelegate, UIGestureRecognizerDelegate {

    private var mapView: MapView!
    private var pointAnnotationManager: PointAnnotationManager!
    private var draggedAnnotation: PointAnnotation?
    private var panGesture: UIPanGestureRecognizer!

    override func viewDidLoad() {
        ...
        let mapView = MapView(...)
        mapView.mapboxMap.onNext(.mapLoaded) { (event) in
            self.mapViewDidLoad(style: self.mapView.mapboxMap.style)
        }
    }

    func mapViewDidLoad(style: Style) {
        ...

        // Setup annotations using the Mapbox provided PointAnnotationManager.
        // Note that the delegate only handles clicks, not drags.
        self.pointAnnotationManager = mapView.annotations.makePointAnnotationManager(id: "annotations")
        self.pointAnnotationManager.delegate = self

        // Annotation drag setup
        self.panGesture = UIPanGestureRecognizer(target: self, action: #selector(handleMapPan(sender:)))
        panGesture.minimumNumberOfTouches = 1
        panGesture.maximumNumberOfTouches = 1
        panGesture.delegate = self

        // Very important: Make sure that other UIPanGestureRecognizers on the mapview only fire
        // when our annotation recognizer has failed. Note we mark it as failed inside handleMapPan
        // if we aren't panning on an annotation.
        for recognizer in mapView.gestureRecognizers! where recognizer is UIPanGestureRecognizer {
            recognizer.require(toFail: panGesture)
        }
        mapView.addGestureRecognizer(panGesture)
    }

    // This is need to let the separate pan gesture recognisers work on both the annotations and
    // the panning over the map.
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return gestureRecognizer == self.panGesture &&
            otherGestureRecognizer == mapView.gestures.panGestureRecognizer
    }

    // Used for handling panning to detect annotation dragging
    @IBAction @objc func handleMapPan(sender: UIPanGestureRecognizer) {

        switch sender.state {
        case .began:
            // Look for features in the annotation layer under the pan movement
            let options = RenderedQueryOptions(layerIds: [ self.pointAnnotationManager.id ], filter: nil)
            mapView.mapboxMap.queryRenderedFeatures(
                at: sender.location(in: sender.view),
                options: options,
                completion: { result in
                    switch result {
                    case .success(let queriedFeatures):

                        // Get the identifiers of all the queried features
                        let queriedFeatureIds: [String] = queriedFeatures.compactMap {
                            guard case let .string(featureId) = $0.feature.identifier else {
                                return nil
                            }
                            return featureId
                        }

                        // Find if any `queriedFeatureIds` match an annotation's `id`
                        let pannedAnnotations = self.pointAnnotationManager.annotations.filter { queriedFeatureIds.contains($0.id) }

                        // If we determined the user has dragged an annotation, store it in a global var and we'll update its
                        // position in the .changed event below.
                        if !pannedAnnotations.isEmpty {
                            self.draggedAnnotation = pannedAnnotations.first!

                        } else {
                            // Very important:
                            // If the user did not pan over any annotations, we need to mark this
                            // gesture as failed, so we can fall through to map panning instead.
                            sender.state = .failed
                        }

                    case .failure:
                        return
                    }
                }
            )

        case .changed:
            guard let annotation = draggedAnnotation else {
                return
            }
            let targetPoint = self.mapView.mapboxMap.coordinate(for: sender.location(in: sender.view))

            // For some reason Mapbox doesn't let us update the geometry of an existing annotation
            // so we have to create a whole new one.
            var newAnnotation = PointAnnotation(id: annotation.id, coordinate: targetPoint)
            newAnnotation.image = annotation.image

            var newAnnotations = self.pointAnnotationManager.annotations.filter { an in
                return an.id != annotation.id
            }
            newAnnotations.append(newAnnotation)
            self.pointAnnotationManager.annotations = newAnnotations

        case .ended:
            if self.draggedAnnotation != nil, let id = Int(self.draggedAnnotation!.id) {
                let targetPoint = self.mapView.mapboxMap.coordinate(for: sender.location(in: sender.view))

                // Optionally notify some other delegate to tell them the drag finished.
                delegate?.mapViewControllerDidDragAnnotation(id, coordinate: targetPoint)

                // Reset our global var containing the annotation currently being dragged
                self.draggedAnnotation = nil
            }
        default:
            return
        }
    }

    // This is standard PointAnnotationManager stuff, unrelated to dragging.
    func annotationManager(_ manager: AnnotationManager, didDetectTappedAnnotations annotations: [Annotation]) {
        ...
    }
rligocki commented 2 years ago

After few hours of struggles, I find out, that it is necessary to change this line of code when using version 10.0.1 let options = RenderedQueryOptions(layerIds: [ self.pointAnnotationManager.id ], filter: nil) to this one let options = RenderedQueryOptions(layerIds: [ self.pointAnnotationManager.layerId ], filter: nil)

Knapiii commented 2 years ago

@samcrawford, I would actually change UIPanGestureRecognizer to UILongPressGestureRecognizer. With UILongPressGestureRecognizer, you get the same features as UIPanGestureRecognizer but you can notify the user that they have grabbed the annotation. (Make the annotation bigger or something).

MapView does not have UILongPressGestureRecognizer, which means that you don't interfere with MapView gestures.

macdrevx commented 2 years ago

Closing in favor of https://github.com/mapbox/mapbox-maps-ios/issues/501