Working with MapKit in SwiftUI
This year, at WWDC2023, Apple announced a new set of MapKit APIs that works a lot better together with SwiftUI.
Let's explore some cool stuff we can achieve if we plan to include a map experience into our app.
Creating a Map
We can include a simple map by creating a Map
instance in our view. This will create a default map.
If we want to add some components to our map, we must use MapContentBuilder
. Inside the closure, we can create any MapContent
component we need.
struct MapContainerView: View {
var body: some View {
Map {
// MapContent
}
}
}
Adding Markers & Annotations
Markers are used to display a specific coordinate in the map. They have a specific UI (balloon shape) that we can not control. However, it's a good alternative if we only need to highlight some coordinates.
extension CLLocationCoordinate2D {
// Some place in Miami
static let coffeeShopCoordinate = CLLocationCoordinate2D(latitude: 25.865208, longitude: -80.121807)
}
struct MapContainerView: View {
var body: some View {
Map {
Marker("Coffee Shop", coordinate: .coffeeShopCoordinate)
}
}
}
The only thing we can change in the markers` UI is the icon displayed.
Marker("Coffee Shop", systemImage: "cup.and.saucer.fill", coordinate: .coffeeShopCoordinate)
Annotations are pretty similar to Markers, but have the flexibility of being able to display a custom SwiftUI view.
struct MapContainerView: View {
var body: some View {
Map {
Annotation("Coffee Shop", coordinate: .coffeeShopCoordinate) {
Circle()
.fill(Color.accentColor)
.frame(width: 30, height: 30)
.overlay {
Image(systemName: "mappin.circle")
.resizable()
.aspectRatio(contentMode: .fit)
}
}
}
}
}
Map Styling
We can control it using mapStyle
modifier.
- Standard: Classic street map.
- Imagery: Based on satellite imagery.
- Hybrid: Combination of the other two. Satellite imagery with street names.
In addition, depending on the style we choose, we can set some other configurations like elevation, point of interest to show (or hide), and traffic insights.
struct MapContainerView: View {
var body: some View {
Map {
Annotation("Coffee Shop", coordinate: .coffeeShopCoordinate) {
Circle()
.fill(Color.accentColor)
.frame(width: 30, height: 30)
.overlay {
Image(systemName: "mappin.circle")
.resizable()
.aspectRatio(contentMode: .fit)
}
}
}
}
.mapStyle(.standard(elevation: .realistic, pointsOfInterest: .including([.cafe]))
}
Interaction Modes
We have four interaction modes that can enable or restrict.
- Pan
- Zoom
- Pitch
- Rotate
struct MapContainerView: View {
var body: some View {
Map(interactionModes: [.pan, .zoom]) {
...
}
}
}
Map Camera Position
If we want to control the position of the map, we must indicate the position when we create the map.
struct MapContainerView: View {
@State private var position: MapCameraPosition
var body: some View {
Map(position: $position) {
...
}
}
}
There are several ways to create a MapCameraPosition
like providing:
MKMapItem
to center the map in the item.MKMapRect
to specify the boundaries.MKMapRegion
to position the map in a specific area.
We can also use the static property automatic
to let the position be determined by the framework according to the content shown on the map.
struct MapContainerView: View {
@State private var position: MapCameraPosition = .automatic
var body: some View {
Map(position: $position) {
...
}
}
}
.safeAreaInset(edge: .top, alignment: .trailing, content: {
RoundedRectangle(cornerRadius: 12)
.fill(Color.white)
.frame(width: 40, height: 40)
.overlay {
Image(systemName: "cup.and.saucer.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
.foregroundStyle(Color.accentColor)
}
.onTapGesture {
withAnimation {
position = .automatic
}
}
.padding(.trailing, 6)
})
MapPolyline
It enables us to connect line segments. We can use it to draw a line from point A to point B.
Like any other MapComponent
, we need to add MapPolyline
component inside MapView
. We can create a MapPolyline
from a MKRoute
, a list of coordinates, or a list of MKMapPoint
.
struct MapContainerView: View {
@State private var route: MKRoute?
var body: some View {
Map {
Annotation("Coffee Shop", coordinate: .coffeeShopCoordinate) {
Circle()
.fill(Color.accentColor)
.frame(width: 30, height: 30)
.overlay {
Image(systemName: "mappin.circle")
.resizable()
.aspectRatio(contentMode: .fit)
}
.onTapGesture {
getDirections()
}
}
if let route {
MapPolyline(route)
.stroke(.blue, lineWidth: 5)
}
}
}
.mapStyle(.standard(elevation: .realistic, pointsOfInterest: .including([.cafe]))
}
extension MapContainerView {
func getDirections() {
route = nil
let request = MKDirections.Request()
request.source = MKMapItem(placemark: .init(coordinate: .userLocationCoordinate))
request.destination = MKMapItem(placemark: .init(coordinate: .coffeeShopCoordinate))
Task {
let directions = MKDirections(request: request)
let response = try? await directions.calculate()
withAnimation {
route = response?.routes.first
}
}
}
}
Look Around Preview
Show preview imagery for a specific location.
This feature is available in some specific regions.
struct MapContainerView: View {
@State private var lookAroundScene: MKLookAroundScene?
var body: some View {
Map {
Annotation("Coffee Shop", coordinate: .coffeeShopCoordinate) {
Circle()
.fill(Color.accentColor)
.frame(width: 30, height: 30)
.overlay {
Image(systemName: "mappin.circle")
.resizable()
.aspectRatio(contentMode: .fit)
}
.onTapGesture {
getlookAroundScene()
}
}
}
}
.mapStyle(.standard(elevation: .realistic, pointsOfInterest: .including([.cafe]))
.safeAreaInset(edge: .bottom, content: {
if let lookAroundScene {
LookAroundPreview(initialScene: lookAroundScene)
.frame(height: 128)
.clipShape(RoundedRectangle(cornerRadius: 10))
.padding([.top, .horizontal])
}
})
}
extension MapContainerView {
func getlookAroundScene() {
lookAroundScene = nil
Task {
let request = MKLookAroundSceneRequest(coordinate: .coffeeShopCoordinate)
lookAroundScene = try? await request.scene
}
}
}
If you want to go deeper into these APIs, I highly recommend you go through Apple's documentation.
There are a lot of different configurations and parameters you can play around with to boost your map experience and make it more powerful.