Working with MapKit in SwiftUI

Bruno Lorenzo
5 min readNov 28, 2023

--

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.

  1. Standard: Classic street map.
  2. Imagery: Based on satellite imagery.
  3. 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.

  1. Pan
  2. Zoom
  3. Pitch
  4. 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.

Have any questions? Feel free to drop me a message! 🙂

  • 🤓 Join me on X for regular content on iOS development tips and insights
  • 🚀 Check out my GitHub where I share all my example projects

--

--

Bruno Lorenzo

Software Engineer | Innovation Manager at www.houlak.com | Former iOS Tech Lead | I write about iOS, tech, and producitivy