Map with Annotations in SwiftUI
Reading time: 6 min
This recipe shows how to add a Map View using SwiftUI. It also shows how to add pins/markers/annotations to the map, allowing for mixing of different map annotation types.
The end result will look like this:
If you just want something you can use right away, you can find the full code here.
SwiftUI Map view is only available starting iOS 14. Unfortunately, you can't use it on iOS 13.
Let's get started!
Displaying a Map
The Map view is responsible for displaying an Apple map. Add this code to get started:
import SwiftUI
import MapKit
struct MapView: View {
// 1
@State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 45.5473973, longitude: 18.6786983),
span: MKCoordinateSpan(latitudeDelta: 0.25, longitudeDelta: 0.25))
var body: some View {
Map(coordinateRegion: $region, // 1
interactionModes: [.all], // 2
showsUserLocation: true, // 3
userTrackingMode: .constant(.follow)) // 4
}
}
Here's a breakdown of what goes on here:
- You can bind the currently displayed region (
MKCoordinateRegion
) of yourMap
to a property. The region will change as the user pans around the map or zooms it. - Determine how the user can interact with the map using
interactionModes:
. The available options arepan
,zoom
andall
. Pass an empty array[]
to prevent the user from modifying the map. - Use
showsUserLocation:
to, well, show the user's location on the map. - If you want the map to automatically move as the user moves, set
userTrackingMode:
to.follow
. You can use a binding here to dynamically turn following on/off.
And that's it! The code above (or an even reduced version of it) is enough to display a fully functioning map view in your app.
Map Annotations
Map annotations allow you to display markers on the map. There are three predefined annotation implementation available in MapKit
:
MapPin
displays a pin with the providedtint
at the given coordinate.MapMarker
is essentially aMapPin
with a solid background.MapAnnotation
allows you to specify a custom SwiftUIView
at the provided coordinates.
Annotations source has to be an array of Identifiable
, so let's add this simple struct:
struct Location: Identifiable {
let id = UUID()
let coordinate: CLLocationCoordinate2D
}
Then, you can add the annotations for the given source like this:
var body: some View {
Map(coordinateRegion: $region,
interactionModes: [.all],
showsUserLocation: true,
userTrackingMode: .constant(.follow)
annotationItems: [ // ADD THIS
Location(coordinate: CLLocationCoordinate2D(latitude: 45.5550119, longitude: 18.6786983)),
Location(coordinate: CLLocationCoordinate2D(latitude: 45.5585655, longitude: 18.6892519))
]) { item in // AND THIS
MapPin(coordinate: item.coordinate)
}
}
Mixing different annotation types
Displaying mixed annotations is a different ballgame. Say that your goal is to show pins, markers and custom annotations on a single map, like this:
Add an enum that models these different annotation types:
enum MapMark {
case pin(tint: Color),
marker(tint: Color),
price(pricing: Int, tint: Color)
}
The enum contains the two default annotation marks, the pin and the marker, as well as a custom one. The custom view in this case simply displays how pricey the location is.
Let's also extend the Location
struct to hold the type:
struct Location: Identifiable {
let id = UUID()
let coordinate: CLLocationCoordinate2D
let mark: MapMark // ADD THIS
}
Then, add a few locations to MapView
:
private let locations = [
Location(
coordinate: CLLocationCoordinate2D(latitude: 45.5550119, longitude: 18.6786983),
mark: .pin(tint: .green)),
Location(
coordinate: CLLocationCoordinate2D(latitude: 45.5585655, longitude: 18.6892519),
mark: .marker(tint: .blue)),
Location(
coordinate: CLLocationCoordinate2D(latitude: 45.5473353, longitude: 18.6890417),
mark: .flag(pricing: 3, tint: .orange)),
]
And add the function that displays the annotation for a Location
:
private func mark(for location: Location) -> some MapAnnotationProtocol {
switch location.mark {
case .pin(tint: let tint):
return MapPin(coordinate: location.coordinate, tint: tint)
case .marker(tint: let tint):
return MapMarker(coordinate: location.coordinate, tint: tint)
case .flag(pricing: let pricing, tint: let tint):
return MapAnnotation(coordinate: location.coordinate) {
Text(String.init(repeating: "$", count: pricing))
.font(.system(size: 12))
.fontWeight(.semibold)
.background(tint.cornerRadius(4))
}
}
}
And, lastly, alter the body
a bit to consume the locations
array and map them to annotations:
var body: some View {
Map(coordinateRegion: $region,
interactionModes: [.all],
showsUserLocation: true,
userTrackingMode: .constant(.follow)
annotationItems: locations) { item in // ADD THIS
mark(for: item)
}
}
If you try to build now, you'll get the familiar error message on the mark
function:
If you haven't, check out this recipe for more info on this type of error. As you'll learn, resolving it involves using a wrapper that erases the protocol (MapAnnotationProtocol
) to a struct.
Unfortunately, for reasons unknown, there's no such wrapper for MapAnnotationProtocol
in MapKit
, so let's try building one ourselves!
struct AnyMapAnnotationProtocol: MapAnnotationProtocol { // 1
let _annotationData: _MapAnnotationData // 2
let value: Any // 3
init<WrappedType: MapAnnotationProtocol>(_ value: WrappedType) { // 4
self.value = value
_annotationData = value._annotationData // 5
}
}
What goes on is as following:
- The wrapper struct needs to conform to
MapAnnotationProtocol
. - Confirming to
MapAnnotationProtocol
requires the presence of the field_annotationData
of type_MapAnnotationData
. However, notice that both of these start with an underscore, meaning that this API is private and Apple isn't ready to share it yet. I don't know why is that the case, just be mindful that this solution is a tad hacky. - We hold a reference to the wrapped value, just in case.
- Make sure that all the values passed are of a type that conforms to
MapAnnotationProtocol
. - This is the core of the wrapper part - copy the
_annotationData
from the wrapped value.
Then, just change the mark
function to wrap all the return values in AnyMapAnnotationProtocol
:
private func mark(for location: Location) -> some MapAnnotationProtocol {
switch location.mark {
case .pin(tint: let tint):
return AnyMapAnnotationProtocol(MapPin(coordinate: location.coordinate, tint: tint))
case .marker(tint: let tint):
return AnyMapAnnotationProtocol(MapMarker(coordinate: location.coordinate, tint: tint))
case .flag(pricing: let pricing, tint: let tint):
return AnyMapAnnotationProtocol)MapAnnotation(coordinate: location.coordinate) {
Text(String.init(repeating: "$", count: pricing))
.font(.system(size: 12))
.fontWeight(.semibold)
.background(tint.cornerRadius(4))
})
}
}
And that's it! Everything should work as expected now. You have a basic framework for modelling annotations and you can display a heterogenous set of map annotations!
Full code for this recipe:
import SwiftUI
import MapKit
struct MapView: View {
@State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 45.5473973, longitude: 18.6786983),
span: MKCoordinateSpan(latitudeDelta: 0.25, longitudeDelta: 0.25))
private let locations = [
Location(
coordinate: CLLocationCoordinate2D(latitude: 45.5550119, longitude: 18.6786983),
mark: .pin(tint: .green)),
Location(
coordinate: CLLocationCoordinate2D(latitude: 45.5585655, longitude: 18.6892519),
mark: .marker(tint: .blue)),
Location(
coordinate: CLLocationCoordinate2D(latitude: 45.5473353, longitude: 18.6890417),
mark: .flag(pricing: 3, tint: .orange)),
]
var body: some View {
Map(coordinateRegion: $region,
interactionModes: [.all],
showsUserLocation: true,
userTrackingMode: .constant(.follow),
annotationItems: locations) { item in
mark(for: item)
}
}
private func mark(for location: Location) -> some MapAnnotationProtocol {
switch location.mark {
case .pin(tint: let tint):
return AnyMapAnnotationProtocol(MapPin(coordinate: location.coordinate, tint: tint))
case .marker(tint: let tint):
return AnyMapAnnotationProtocol(MapMarker(coordinate: location.coordinate, tint: tint))
case .flag(pricing: let pricing, tint: let tint):
return AnyMapAnnotationProtocol(MapAnnotation(coordinate: location.coordinate) {
Text(String.init(repeating: "$", count: pricing))
.font(.system(size: 12))
.fontWeight(.semibold)
.background(tint.cornerRadius(4))
})
}
}
}
private struct Location: Identifiable {
let id = UUID()
let coordinate: CLLocationCoordinate2D
let mark: MapMark
}
private enum MapMark {
case pin(tint: Color),
marker(tint: Color),
flag(pricing: Int, tint: Color)
}
private struct AnyMapAnnotationProtocol: MapAnnotationProtocol {
let _annotationData: _MapAnnotationData
let value: Any
init<WrappedType: MapAnnotationProtocol>(_ value: WrappedType) {
self.value = value
_annotationData = value._annotationData
}
}