SwiftUI iOS 14 MapKit Annotation Tap Gesture (didSelect)

After spending two hours trying to find anything on it (and failing), I hope for a last resort here:

I have a very simple map with annotation markers. Problem: the user taps on a marker or pin — and in the past it used to fire a "didSelect" on the mapView, but now?! How do I react to the user tapping a pin or marker? I tried to add a .onTapGesture but no such thing.

Code Block swiftui
Map(
  coordinateRegion: $viewMapModel.mapLocation,
  annotationItems: viewMapModel.annotations,
  annotationContent: {
n in MapMarker(
coordinate: n.location,
tint: .red
)
}
)


Essentially, once a user taps a pin or marker, I want to show a popup, but just showing a print would already make my world for simplicity purposes here.

I want to use this new iOS 14+ format, so please no solutions taking me back to makeUIView->MKMapView stuff. Thanks!

Replies

Has anyone been able to figure this out with iOS 14 out now? Are annotations simply not tappable?
Have you tried MapAnnotation?

You need to construct the shape of the annotation by yourself, but as the content of MapAnnotation is a View, you might be able to use onTapGesture.
@OOPer thanks for responding. Unfortunately that also doesn't work.

Code Block
Map(
        coordinateRegion: $viewMapModel.mapLocation,
        interactionModes: .all,
        showsUserLocation: true,
        userTrackingMode: $userTrackingMode,
        annotationItems: viewMapModel.annotations,
        annotationContent: {
          n in MapAnnotation(coordinate: n.location) {
            Circle()
            .fill(Color.green)
            .frame(width: 44, height: 44)
            .onTapGesture(count: 1, perform: {
              print("IT WORKS")
            })
          }
        }
      )


Does not react to the tap.

Also, can't attach a onTapGesture to the MapAnnotation [some view], compiler says no.

Unfortunately that also doesn't work.

Thanks for testing and sharing the result.

With hearing it does not work, I'm afraid you may need to send a feature request and wait for new iOS. Or, go back to UIViewRepresentable.
I have the same problem on tapping MapAnnotations. I am looking for a solution for iOS 14+ as well. Any help would be appreciated.

Code Block
Map(coordinateRegion: $locationStore.locationCoordinate,
                interactionModes: MapInteractionModes.all, showsUserLocation: true,
                userTrackingMode: $userTrackingMode,
                annotationItems: locationStore.retailer) { place in
                MapAnnotation(coordinate: place.coordinate()) {
                    MapAnnotationView(retailer: place)
                        .onTapGesture(count: 1, perform: {
                            self.place = place
                            self.showingAlert = true
                        })
                }
            }.edgesIgnoringSafeArea(.all)


i'm also curious is anyone was able to figure this out.
iOS 14 SwiftUI Map() is missing a lot of the functionality of MKMapView. I just tried to create a "pure" SwiftUI app, with Map(), for converting a sequence of taps on a map into a polyline. TapGesture does not report the View location of the tap, nor does Map() have access to the convert to coordinate method. I too was reluctant to return to the UIViewRepresentable approach.

In developing SwiftUI apps I always use a singleton DataModel, as an ObservableObject with Published properties. For my recent mapping app I therefore added MKMapViewDelegate and UIGestureRecognizerDelegate to the DataModel class and created a mapView = MKMapView() in the model, then added all necessary delegate functions. All processing of taps, polyline points etc occur in the model. I often use Combine's PassthroughSubject (though not in this case, wasn't needed) to trigger some behaviour in a SwiftUI View, via .onReceive.

The UIViewRepresentable is then as simple as:

struct MapView: UIViewRepresentable {

    func makeUIView(context: UIViewRepresentableContext<MapView>) -> MKMapView {
        return DataModel.shared.mapView
    }

    func updateUIView(_ view: MKMapView, context: UIViewRepresentableContext<MapView>) {
        view.setRegion(DataModel.shared.region, animated: true)
    }
}

There's no need for a Coordinator, and panning, zooming etc all work fine along with the Tap recogniser.

I realise that this is not what you're seeking, but the approach might be useful given the limitations of Map().

Regards, Michaela
Hi,

I made it working. Hope it covers your use case. Please check below:

Code Block swift
import MapKit
import SwiftUI
struct ContentView: View {
    @State private var locations: [Mark] = []
    @State private var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(
            latitude: 25.7617,
            longitude: 80.1918
        ),
        span: MKCoordinateSpan(
            latitudeDelta: 10,
            longitudeDelta: 10
        )
    )
    var body: some View {
        ZStack {
            Map(coordinateRegion: $region, annotationItems: locations) { location in
                MapAnnotation(
                    coordinate: location.coordinate,
                    anchorPoint: CGPoint(x: 0.5, y: 0.7)
                ) {
                    VStack{
                        if location.show {
                            Text("Test")
                        }
                        Image(systemName: "mappin.circle.fill")
                            .font(.title)
                            .foregroundColor(.red)
                            .onTapGesture {
                                let index: Int = locations.firstIndex(where: {$0.id == location.id})!
                                locations[index].show.toggle()
                            }
                    }
                }
            }
            Circle()
                .fill(Color.blue)
                .opacity(0.3)
                .frame(width: 32, height: 32)
            VStack {
                Spacer()
                HStack {
                    Spacer() 
                    Button(action: {
                        locations.append(Mark(coordinate: region.center))
                    }) {
                        Image(systemName: "plus")
                    }
                    .padding()
                    .background(Color.black.opacity(0.7))
                    .foregroundColor(.white)
                    .font(.title)
                    .clipShape(Circle())
                    .padding(20)
                }
            }
        }
        .ignoresSafeArea()
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
struct Mark: Identifiable {
    let id = UUID()
    let coordinate: CLLocationCoordinate2D
    var show = false
}


  • This does not work if you want to have the map be tappable as well, to create new markers. In this case, a tap event on the annotation "bubbles" up to the map as well, creating a new marker.

Add a Comment
This still doesn't cover the problem of how do you get the location touched on the map. There are missing parameters to the onGestureXXXX() chained calls. These should all have lat/lon of where the gesture occurred at on the map.
Could you please try with iOS 14.2+ build and let us know if the issue still persists.
  • Just confirmed that .onTapGesture() works for Map in iOS 15. But it doesn't work for MapPin.

Add a Comment
Interesting thread. I'm getting the impression that there are a lot of hurdles refactoring existing maps to native Swift UI Map.

I need for example Clustering.

With UIViewRepresentable approach I could do something like:

Code Block
let annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Fountain") annotationView.clusteringIdentifier = "FountainCluster"


Was anyone successful clustering MapPin, MapMarker or MapAnnotation - and defining interactions for the cluster (e.g. tap to expand/zoom in)?

  • Do SwiftUI maps support clustering now?

Add a Comment

Hopefully this will be implemented in next ios. Has anyone found a good workaround yet?

A solution is to create a struct that signs MapAnnotationProtocol with some Any type attributes. Then call MapAnnotation with that struct. So onTapGesture works.

1) Create a struct that conforms to MapAnnotationProtocol.

struct AnyMapAnnotationProtocol: MapAnnotationProtocol {
  let _annotationData: _MapAnnotationData
  let value: Any

  init<WrappedType: MapAnnotationProtocol>(_ value: WrappedType) {
    self.value = value
    _annotationData = value._annotationData
  }
}

2) Call the new SwiftUI Map View's closure, then call the annotation previously set.

 Map(coordinateRegion: $region, interactionModes: [.all], showsUserLocation: false, userTrackingMode: .constant(.follow), annotationItems: annotatedStations) { item in
AnyMapAnnotationProtocol(MapAnnotation(coordinate: itemCoordinate) {
                HStack {
                    Image("byeByeDidSelect")
                        .resizable()
                        .frame(width: 45, height: 40)
                        .clipShape(Capsule())
                    Text("This annotation can be tapped.")
                        .foregroundColor(.black)
                }
                .onTapGesture {
                    print("Test tapping")
                }
            })
}

Hope help you guys.

This works. Thank you, @ramonteiro

However, change MapAnnotation(coordinate: itemCoordinate to MapAnnotation(coordinate: item.Coordinate and make sure to have an image "byeByeDidSelect" in your assets.