How do I make a clickable pin in SwiftUI's new Map view?

Viewed 4645

I'm using SwiftUI new Map view to display pins for annotations, and would like the pins, when clicked, to display a view for editing the annotation's name and description.

I've tried to use MapAnnotation with a view that contains a button with a pin image. The image displays correctly, but the button doesn't work.

Is it possible to do this without falling back to a UIViewRepresentable of MKMapView?

import SwiftUI
import MapKit

struct ContentView: View {
    @State private var showingEditScreen = false

    @State private var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 37.334722,
                                       longitude: -122.008889),
        span: MKCoordinateSpan(latitudeDelta: 1,
                               longitudeDelta: 1)
    )

    @State private var pins: [Pin] = [
        Pin(name: "Apple Park",
            description: "Apple Inc. headquarters",
            coordinate: CLLocationCoordinate2D(latitude: 37.334722,
                                               longitude:-122.008889))
    ]

    var body: some View {
        Map(coordinateRegion: $region,
            interactionModes: .all,
            annotationItems: pins,
            annotationContent: { pin in
                MapAnnotation(coordinate: pin.coordinate,
                              content: {
                                PinButtonView(pin: pin)
                              })
            })
    }
}

My Pin definition:

struct Pin: Identifiable {
    let id = UUID()
    var name: String
    var description: String
    var coordinate: CLLocationCoordinate2D

}

The view with the button and pin image:

struct PinButtonView: View {
    @State private var showingEditScreen = false
    @State var pin: Pin

    var body: some View {
        Button(action: {
            showingEditScreen.toggle()
        }) {
            Image(systemName: "mappin")
                .padding()
                .foregroundColor(.red)
                .font(.title)
        }
        .sheet(isPresented: $showingEditScreen,
               content: {
                EditView(pin: self.$pin)
               })
    }
}

Editing view:

struct EditView: View {
    @Environment(\.presentationMode) var presentationMode

    @Binding var pin: Pin

    var body: some View {
        NavigationView {
            Form {
                    TextField("Place name", text: $pin.name)

                    TextField("Description", text: $pin.description)
            }
            .navigationTitle("Edit place")
            .navigationBarItems(trailing: Button("Done") {
                self.presentationMode.wrappedValue.dismiss()
            })
        }
    }
}
3 Answers

I have a SwiftUI MapKit view that uses MapAnnotations with a pin that is tappable and it displays a View with information in.

It is not pretty but it works for now iOS 14 only.

https://github.com/PhilStollery/BAB-Club-Search/blob/main/Shared/views/AnnotatedMapView.swift

import MapKit
import SwiftUI

struct AnnotatedMapView: View {
    
    @ObservedObject
    private var locationManager = LocationManager()
    
    // Default to center on the UK, zoom to show the whole island
    @State private var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 54.4609,
                                       longitude: -3.0886),
        span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10))
    
    @ObservedObject var store: ClubStore
    
    var body: some View {
        ZStack {
            Map(coordinateRegion: $region,
                showsUserLocation: true,
                annotationItems: store.clubs!) {
                    club in MapAnnotation(coordinate: club.coordinate) {
                        VStack{
                            if club.show {
                                ZStack{
                                    RoundedRectangle(cornerRadius: 10)
                                        .fill(Color(UIColor.systemBackground))
                                        .shadow(radius: 2, x: 2, y: 2)
                                    VStack{
                                        NavigationLink(destination: ClubLocationView(club: club)) {
                                            Text(club.clubname)
                                                .fontWeight(.bold)
                                        }
                                        .padding()
                                        Text(club.association)
                                            .padding(.leading)
                                            .padding(.trailing)
                                        Text(club.town)
                                            .padding(.bottom)
                                    }
                                }
                                .onTapGesture {
                                    let index: Int = store.clubs!.firstIndex(where: {$0.id == club.id})!
                                    store.clubs![index].show = false
                                }
                            } else {
                                Image(systemName: "house.circle")
                                    .font(.title)
                                    .foregroundColor(.accentColor)
                                    .onTapGesture {
                                        let index: Int = store.clubs!.firstIndex(where: {$0.id == club.id})!
                                        store.clubs![index].show = true
                                    }
                            }
                        }
                    }
            }
            .edgesIgnoringSafeArea(.bottom)
        }
        .navigationBarTitle(Text("Map View"), displayMode: .inline)
        .navigationBarItems(trailing:
                                Button(action: {
                                    withAnimation {
                                        self.region.center = locationManager.current!.coordinate
                                        self.region.span = MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)
                                    }
                                })
                                    {Image(systemName: "location")}
        )
    }
}

As of XCode 12.3 this appears to be functioning with buttons. One key gotcha that I noticed though was that if you use offsets then it's possible your buttons will be placed out of the tappable area of the annotation.

In order to counter that you can add additional padding to account for the offset and keep it within tappable bounds:

MapAnnotation(coordinate: item.placemark.coordinate) {
  ZStack {
      MapPinView() // My view for showing a precise pin
      VStack { // A prompt with interactive option, offset by 60pt
          Text("Use this location?")
          HStack {
              Button("Yes", action: {
                  print("yes")
              })
              
              Button("No", action: {
                  print("no")
              })
          }
      }
      .offset(x: 0, y: 60) // offset prompt so it's below the marker pin
  }
  .padding(.vertical, 60) // compensate for offset in tappable area of annotation. pad both the top AND the bottom to keep contents centered
}

With Xcode 12 GM map pins still can not be intractable.

Related