Adding Map Controls to a Map view with SwiftUI and MapKit

Adding Map Controls to a Map view with SwiftUI and MapKit

Learn how to add map controls to a map in a SwiftUI application.

MapKit features many controls that allow the user to interact with the map easily:

  • MapCompass displays the current orientation of the associated map
  • MapPitchToggle sets the pitch of the associated map (the camera angle as 3D or 2D)
  • MapScaleView shows a legend with distance information when the user zooms in and out (requires the elevation to be set as realistic)
  • MapUserLocationButton frames the map to the user's current location (requires authorization to use the user's location)

Some controls are available only for macOS though, replacing actions that on iPhone and iPad the users do with gestures, like:

  • MapPitchSlider allows the user to change the pitch of the map (macOS only)
  • MapZoomStepper allows the user to adjust the zoom level(macOS only)

To add these controls to a Map view on SwiftUI you can use a modifier or use them as standalone views that share the same scope of the Map.

Let’s use the following code as a starting point:

import SwiftUI
import MapKit

struct ContentView: View {
    
    let garage = CLLocationCoordinate2D(
        latitude: 40.83657722488077,
        longitude: 14.306896671048852
    )
    
    @State private var position: MapCameraPosition = .userLocation(fallback: .automatic)
    
    var body: some View {
    
        Map(position: $position) {
            Marker("Garage", coordinate: garage)
        }
        .mapStyle(.standard(elevation: .realistic))
        
    }
}

It creates a Map view with an annotation marker, a position for the map camera, and sets up the elevation to be realistic rendering the map in 3D. A map style is initialized with a MapStyle object.


Be aware that to access the user's location, authorization has to be requested for privacy reasons. This requires editing the Info section of your project settings and implementing a CoreLocation manager to handle the requests and updates for the user's location.

Read more about it in the following article at the official Apple documentation.

Requesting authorization to use location services | Apple Developer Documentation
Obtain authorization to use location services and manage changes to your app’s authorization status.

Using the mapControls(_:) modifier

The first way of adding controls to a Map view is by using the mapControls(_:) modifier. It allows you to define the controls that will appear on top of the map without having to specify a scope for them.

Map(position: $position) {
    Marker("Garage", coordinate: garage)
}

.mapControls {
    /// Shows up when you pitch to zoom
    MapScaleView()
    /// Shows up when you rotate the map
    MapCompass()
    /// 3D and 2D button on the top right
    MapPitchToggle()
}

.mapStyle(.standard(elevation: .realistic))

Using map controls as views

You can also use the map controls as views in a view hierarchy, but then you need to associate them with a map via a scope. To achieve it you need to:

  • Define a namespace in your view
  • Set it up using the mapScope(_:) modifier
  • Associate the scope to the Map view
  • Connect the map controls you add with the map by assigning the same scope

Here is an example:

import SwiftUI
import MapKit

struct ContentView: View {
    
    let garage = CLLocationCoordinate2D(
        latitude: 40.83657722488077,
        longitude: 14.306896671048852
    )
    
    @State private var position: MapCameraPosition = .userLocation(fallback: .automatic)
    
    /// Create a namespace for the map to work at
    @Namespace var mapScope
    
    var body: some View {
        ZStack(alignment: .topTrailing) {
            
            /// Associates the scope to the map view
            Map(position: $position, scope: mapScope) {
                Marker("Garage", coordinate: garage)
            }
            .mapStyle(.standard(elevation: .realistic))
            
            /// Adds a control and connects it to the map scope
            MapPitchToggle(scope: mapScope)
                .background(
                    RoundedRectangle(cornerRadius: 10)
                        .foregroundColor(.white)
                )
                .shadow(radius: 10)
                .padding()
        }
        /// Creates a map scope to connect map controls to an associated map
        .mapScope(mapScope)
        
    }
}