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