'Adding Callout to Pin in swiftUI Map

I'm working on my first map app in swift and I'm a bit stuck. I have gotten this far but I'm not sure how to get callouts to show up when I click on the pin. I read about using .onTapGesture and can get a print out in the console when I do that, but I'm a bit lost in getting a callout to show up when clicking on the point that is on the map. Can someone point me in the right direction or even tell me if the current code I have will work with callouts?

import MapKit
import SwiftUI

let mapView = MKMapView()

struct ContentView: View {

  @State private var locations: [Location] = []

  @State private var coordinateRegion = MKCoordinateRegion(
    center: CLLocationCoordinate2D(latitude: 32.605, longitude: -85.4875),
    span: MKCoordinateSpan(latitudeDelta: 0.013, longitudeDelta: 0.013)
    )
  
  @State private var showTitle = true
    
    

  var body: some View {
      Text("Auburn Campus Tour").font(.title)
    
    Map(
      coordinateRegion: $coordinateRegion,
      showsUserLocation: true, annotationItems: locations
    ) {location in
      MapAnnotation(
        coordinate: CLLocationCoordinate2D(
          latitude: location.latitude,
          longitude: location.longitude
        )
      ) {
        VStack {
          Image("au")
                .resizable()
                .edgesIgnoringSafeArea(.top)
                .frame(width: 35, height: 30)
          Text(location.name)
                .font(.system(size: 12))
                .bold()
            }.onTapGesture {
//                AssetAnnotationView(au)
            }
      }
    }
    .onAppear(perform: readFile)
    .onAppear{
        MKMapView.appearance().mapType = .standard
        MKMapView.appearance().showsPointsOfInterest = false
        print("Annotations Loaded From File")
        print("latitude: 32.60, longitude: -85.4875")
        
    
    }
}



  private func readFile() {
    if let url = Bundle.main.url(forResource: "auburn", withExtension: "json"),
       let data = try? Data(contentsOf: url) {
      let decoder = JSONDecoder()
      if let jsonData = try? decoder.decode(JSONData.self, from: data) {
        self.locations = jsonData.locations
      }
    }
  }


struct JSONData: Decodable {
  let locations: [Location]
}
    
    struct AssetAnnotationView: View {
        var image: UIImage
        var body: some View {
            Image(uiImage: image)
                .resizable()
                .aspectRatio(contentMode: .fill)
                .frame(width: 100, height: 100)
                .cornerRadius(3.0)
                .padding(4)
            }
        }
    }

struct Location: Decodable, Identifiable {
  let id: Int
  let name: String
  let latitude: Double
  let longitude: Double
  let desc: String
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
    
}


Solution 1:[1]

The easiest way is to make a view struct for your pin. Then you can just drop it into place.

This is an example of a view for the pin:

@State private var showCallout = false

    let title: String
    let subtitle: String?
    let color: UIColor
    @Binding var isSelected: Bool?
    
    init(title: String, subtitle: String? = nil, color: UIColor, isSelected: Binding<Bool?> = .constant(nil)) {
        self.title = title
        self.subtitle = subtitle
        self.color = color
        self._isSelected = isSelected
    }
    
    var body: some View {
        VStack(spacing: 0) {
            VStack {
                Text(title)
                    .font(.callout)
                    .padding(5)
                
                Text(subtitle ?? "")
                    .font(.callout)
                    .padding(5)
                    .opacity(showCallout && (subtitle != nil) ? 1 : 0)
            }
            .background(Color(.white))
            .cornerRadius(10)
            .opacity(showCallout ? 1 : 0)
            
            Image(systemName: "exclamationmark.circle.fill")
                .font(.title)
                .foregroundColor(Color(uiColor: color))
            
            Image(systemName: "arrowtriangle.down.fill")
                .font(.caption)
                .foregroundColor(Color(uiColor: color))
                .offset(x: 0, y: -5)
        }
        .onTapGesture {
            showCallout.toggle()
        }
        .onLongPressGesture {
            if let _ = isSelected {
                self.isSelected!.toggle()
            }
        }
    }
}

You would use it like this:

Map(
  coordinateRegion: $coordinateRegion,
  showsUserLocation: true, annotationItems: locations
) {location in
  MapAnnotation(
    coordinate: CLLocationCoordinate2D(
      latitude: location.latitude,
      longitude: location.longitude
    )
  ) {
        MapPin(title: location.name, subtitle: location.desc, color: Color.red)
    }

This will show the callout when tapped, and as a bonus, provides a binding for a long press selection of the annotation.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Yrb