UIKit Custom Annotation with Callout in SwiftUI

DevTechie Inc
Jun 24, 2022

Annotation views in SwiftUI provide some basic functionality, but if you are looking for more than a custom view to define your annotation, SwiftUI falls a bit short there. But, UIKit is to the rescue. By using UIKit’s MapView we can bring all that powerful customization to SwiftUI.

In this article, we will bring custom annotations to SwiftUI. You will have this 👇 by the end of this article.

Let’s start with a data structure.

We will create a class called MapPlace which will contain our location information. This class will also be responsible to provide sample data so we can drop annotations for those locations. We will also make this class conform to MKAnnotation protocol so we can use it directly for Annotation View.

Our class will look like this:

final class MapPlace: NSObject, Identifiable {
    let id = UUID()
    let name: String
    let image: String
    let location: CLLocation
    
    init(name: String, image: String, location: CLLocation) {
        self.name = name
        self.image = image
        self.location = location
    }
}
Sample data will go in an extension to this class as shown below:

extension MapPlace {
    static var mapPlaces: [MapPlace] {
        [
            MapPlace(name: "Golden Gate Bridge", image: "GG", location: CLLocation(latitude: 37.83266647135866, longitude: -122.47709319891423)),
            MapPlace(name: "Statue of Liberty", image: "SOL", location: CLLocation(latitude: 40.700005935766036, longitude: -74.044917569399)),
            MapPlace(name: "Eiffel Tower", image: "ET", location: CLLocation(latitude: 40.748570526797614, longitude: -73.98560002833449)),
            MapPlace(name: "Gilroy Garlic", image: "GGarlic", location: CLLocation(latitude: 36.988917901109794, longitude: -121.55013690759337)),
            MapPlace(name: "Sea World", image: "SW", location: CLLocation(latitude: 32.761804202349005, longitude: -117.2255336727055))
        ]
    }
}
Let’s make this class conform to the MKAnnotation protocol where coordinate is the only required property but we will also provide Annotation title information:

extension MapPlace: MKAnnotation {
    var coordinate: CLLocationCoordinate2D {
        location.coordinate
    }
    
    var title: String? {
        name
    }
}
Now we are ready to port UIKit’s MapView into SwiftUI. We will create a struct conforming to UIViewRepresentable. This struct will have three properties as shown below

struct UIKitMapView_Annotation: UIViewRepresentable {
    let region: MKCoordinateRegion
    let mapType: MKMapType
    let places: [MapPlace]
    
}
UIViewRepresentable protocol requires two functions to be implemented, makUIView function is used to create object instance for the view and updateUIView function is used to update object to reflect new updates. We will add them as shown below:

struct UIKitMapView_Annotation: UIViewRepresentable {
    let region: MKCoordinateRegion
    let mapType: MKMapType
    let places: [MapPlace]
    
    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.setRegion(region, animated: true)
        mapView.mapType = mapType
        mapView.isRotateEnabled = false
        mapView.delegate = context.coordinator // conform to delegate
        mapView.addAnnotations(places) // add annotations to map
        return mapView
    }
    
    func updateUIView(_ uiView: MKMapView, context: Context) {
        uiView.mapType = mapType
    }
    
}
Highlighted lines in the code above are important,

  • mapView.delegate makes sure that our view has ability to respond to changes received by the delegate(which we still have to build)
  • mapView.addAnnotations, adds annotations on to the map and because our MapPlace conforms to MKAnnotation protocol, this means we can directly pass the list of annotations to addAnnotations function.
Delegate conformance works a little different in the world of SwiftUI vs in UIKit. In SwiftUI, while porting UIKit views over, we have coordinators that can act as delegates and can provide the implementation for those delegate functions. This will become more clear with the example below:

struct UIKitMapView_Annotation: UIViewRepresentable {
    let region: MKCoordinateRegion
    let mapType: MKMapType
    let places: [MapPlace]
    
    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.setRegion(region, animated: true)
        mapView.mapType = mapType
        mapView.isRotateEnabled = false
        mapView.delegate = context.coordinator
        mapView.addAnnotations(places)
        return mapView
    }
    
    func updateUIView(_ uiView: MKMapView, context: Context) {
        uiView.mapType = mapType
    }
    
    func makeCoordinator() -> MapCoordinator {
        MapCoordinator()
    }
    
    final class MapCoordinator: NSObject, MKMapViewDelegate {
        func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
            guard let mapPlace = annotation as? MapPlace else { return nil }
            
            let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "customMapAnnotation") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "customMapAnnotation")
            
            annotationView.canShowCallout = true
            annotationView.glyphText = "⛳️"
            annotationView.markerTintColor = .systemPink
            annotationView.titleVisibility = .visible
            annotationView.detailCalloutAccessoryView = UIImage(named: mapPlace.image).map(UIImageView.init)
            
            return annotationView
        }
    }
}
As shown in the highlighted code above, we implement function called makeCoordinator and create an instance of MapCoordinator.

MapCoordinator is a class which will conform to MKMapViewDelegate which in this case will give us ability to implement custom view for our annotation.

Last but not the least, we use this new view in SwiftUI as shown below:

import SwiftUI
import MapKitstruct CustomMapAnnotations: View {
    @State private var region = MKCoordinateRegion(center: MapPlace.mapPlaces[0].coordinate, latitudinalMeters: 5000, longitudinalMeters: 5000)
    @State private var mapType: MKMapType = .hybrid
    
    var body: some View {
        UIKitMapView_Annotation(region: region, mapType: mapType, places: MapPlace.mapPlaces)
            .ignoresSafeArea(.all)
    }
}
Output will look like this:




With that we have reached the end of this article. Thank you once again for reading. Subscribe to our weekly newsletter at https://www.devtechie.com