Beta
Maps SDK for iOS v10

Migrate to v10

The Mapbox Maps SDK v10 introduces improvements to how Mapbox works on iOS platforms, as well as changes to how developers use the SDK. This document summarizes the most important changes and walks you through how to upgrade an application using a previous version of the Mapbox Maps SDK to v10.

Requirements

  • Xcode 12.2
  • Swift version 5.3+
  • iOS 11 or greater
Note

We do not support building with Apple Silicon. Support may be available later in the Public Beta period.

New MapView

Structured API surface

The Maps SDK v10 aims to provide a single point of entry to all map-related tasks. The API interface of the new MapView is now hierarchical in nature. Methods are strictly separated based on their components (for example Style, Camera, and Location).

For example, all capabilities related to style will be part of the style property of the MapView. Other capabilities like annotations or user location have their own corresponding properties that encapsulate their feature set.

Display a map

The primary interface in v10 is the MapView, which, like in pre-v10, is a UIView.

The example below displays a full-screen map with a default style. Initializing a MapView creates a default set of map options, providing a strong foundation.

pre-v10:

import UIKit
import Mapbox

class BasicMapViewController: UIViewController {
    
    var mapView: MGLMapView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        mapView = MGLMapView(frame: view.bounds)
        view.addSubview(mapView)
    }
}

v10:

import UIKit
import MapboxMaps

class BasicMapViewController: UIViewController {

    var mapView: MapView!

    override func viewDidLoad() {
        super.viewDidLoad()
        let myResourceOptions = ResourceOptions(accessToken: "YOUR_ACCESS_TOKEN")
        mapView = MapView(with: view.bounds, resourceOptions: myResourceOptions)
        view.addSubview(mapView)
    }
}

The result will look like this:

Listening to the map's lifecycle

The Map has several stages to its lifecycle and communicates transitions via a callback mechanism. This callback mechanism replaces the MGLMapViewDelegate protocol seen in pre-v10 versions of the Maps SDK.

The container view controller in the last example can listen for when the map has finished loading using the following code:

class BasicMapViewController: UIViewController {

    var mapView: MapView!

    override func viewDidLoad() {
        super.viewDidLoad()
        let myResourceOptions = ResourceOptions(accessToken: "YOUR_ACCESS_TOKEN")
        mapView = MapView(with: view.bounds, resourceOptions: myResourceOptions)
        view.addSubview(mapView)

        /**
         The closure is called whenever the map starts loading,
         including when a new style has been set and the map must reload.

         This is very early in the lifecycle of the map and should not
         be used to perform any tasks that require a valid renderer.
        */
        mapView.on(.mapLoadingStarted) { (event) in
            print("The map has started loading... Event = \(event)")
        }

        /**
         The closure is called during the initialization of the map view and after any
         subsequent loading of a new style. This method is called between the
         `-mapViewWillStartRenderingMap:` and `-mapViewDidFinishRenderingMap:` delegate
         methods. Changes to sources or layers of the current style do not cause this
         method to be called.

         This method is the earliest opportunity to modify the layout or appearance of
         the current style before the map view is displayed to the user. Adding a layer at
         this time to the map may result in the layer being presented BEFORE the rest of the
         map has finished rendering.
         */

        mapView.on(.styleLoadingFinished) { (event) in
            print("The map has finished loading style data... Event = \(event)")
        }

        /**
         The closure is called whenever the map finishes loading, either after the
         initial load OR after a style change has forced a reload.

         This is the ideal place to add any runtime styling or annotations
         to the map and ensures that these layers would only be shown after
         the map has been fully rendered.
         */
        mapView.on(.mapLoadingFinished) { (event) in
            print("The map has finished loading... Event = \(event)")
        }

        /**
        The closure is called whenever the map view is entering an idle state,
         and no more drawing will be necessary until new data is loaded
         or there is some interaction with the map.

         - All currently requested tiles have loaded
         - All fade/transition animations have completed
        */
        mapView.on(.mapIdle) { (event) in
            print("The map is idle... Event = \(event)")
        }

        /**
         The closure is called whenever the map has failed to load.
         This could be because of a variety of reasons, including a network connection
         failure or a failure to fetch the style from the server.

         You can use the given error message to notify the user that map
         data is unavailable.
         */
        mapView.on(.mapLoadingError) { (event) in
            print("The map failed to load.. Event = \(event)")
        }
    }
}

Map options

Configure the map

The map created in the example above is initialized with a set of reasonable defaults that can be changed by a closure-based API.

The example below shows the update mechanism present on the MapView. The closure passed in is called with the current map options on the map. Once the updated map options are registered, the MapView reloads only those components that have been changed.

pre-v10:

In pre-v10 versions of the Maps SDK, options were set on the MGLMapView or using an MGLMapViewDelegate method.

private var restrictedBounds: MGLCoordinateBounds!

override func viewDidLoad() {
    super.viewDidLoad()

    let mapView = MGLMapView(frame: view.bounds)
    mapView.delegate = self

    mapView.showsScale = YES;
    mapView.pitchEnabled = YES;

    view.addSubview(mapView)
}

func mapView(_ mapView: MGLMapView, shouldChangeFrom oldCamera: MGLMapCamera, to newCamera: MGLMapCamera) -> Bool {

    let currentCamera = mapView.camera
    let newCameraCenter = newCamera.centerCoordinate
    mapView.camera = newCamera
    let newVisibleCoordinates = mapView.visibleCoordinateBounds
    mapView.camera = currentCamera

    // Test if the newCameraCenter and newVisibleCoordinates are inside self.restrictedBounds.
    let inside = MGLCoordinateInCoordinateBounds(newCameraCenter, self.restrictedBounds)
    let intersects = MGLCoordinateInCoordinateBounds(newVisibleCoordinates.ne, self.colorado) && MGLCoordinateInCoordinateBounds(newVisibleCoordinates.sw, self.restrictedBounds)

    return inside && intersects
}

v10:

In the example below, the MapView recognizes that the ornaments, gestures, and camera components have been updated and selectively reloads the corresponding infrastructure.

mapView.update { (mapOptions) in
    // Configure map to show a scale bar
    mapOptions.ornaments.showsScale = true

    // Configure map to disable pitch gestures
    mapOptions.gestures.pitchEnabled = false

    // Configure map to restrict panning to a set of coordinate bounds
    mapOptions.camera.restrictedCoordinateBounds = someBounds
}

Modular architecture

The Maps SDK v10 is being developed with a goal of providing a modular architecture. This sets the foundation for a future plugin-based architecture that can be minimized or extended.

Example: Replaceable HTTP stack

You can supply custom implementations for some components of the SDK.

The example below shows you how to replace the HTTP stack. You must replace the stack early in the application lifecycle, before any API requests are made, and you should call setUserDefinedForCustom once only.

import UIKit
import MapboxMaps

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    let customHTTPService = CustomHttpService()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        try! HttpServiceFactory.setUserDefinedForCustom(customHTTPService)
        return true
    }

    ...

CustomHttpService referenced in the code snippet above implements HttpServiceInterface. The following is an example of a partial implementation:

class CustomHttpService: HttpServiceInterface {

    // MARK: - HttpServiceInterface protocol conformance

    func request(for request: HttpRequest, callback: @escaping HttpResponseCallback) -> UInt64 {
        // Make an API request
        var urlRequest = URLRequest(url: URL(string: request.url)!)

        let methodMap: [HttpMethod: String] = [
            .get : "GET",
            .head : "HEAD",
            .post : "POST"
        ]

        urlRequest.httpMethod          = methodMap[request.method]!
        urlRequest.httpBody            = request.body
        urlRequest.allHTTPHeaderFields = request.headers

        let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in

            // `HttpResponse` takes an `MBXExpected` type. This is very similar to Swift's
            // `Result` type. APIs using `MBXExpected` are prone to future changes.
            let expected: MBXExpected<HttpResponseData, HttpRequestError>

            if let error = error {
                // Map NSURLError to HttpRequestErrorType
                let requestError = HttpRequestError(type: .otherError, message: error.localizedDescription)
                expected = MBXExpected(error: requestError)
            }
            else if let response = response as? HTTPURLResponse,
                    let data = data {

                // Keys are expected to be lowercase
                var headers: [String: String] = [:]
                for (key, value) in response.allHeaderFields {
                    guard let key = key as? String,
                          let value = value as? String else {
                        continue
                    }

                    headers[key.lowercased()] = value
                }

                let responseData = HttpResponseData(headers: headers, code: Int64(response.statusCode), data: data)
                expected = MBXExpected(value: responseData)
            }
            else {
                // Error
                let requestError = HttpRequestError(type: .otherError, message: "Invalid response")
                expected = MBXExpected(error: requestError)
            }

            let response = HttpResponse(request: request, result: expected as! MBXExpected<AnyObject, AnyObject>)
            callback(response)
        }

        task.resume()

        // Handle used to cancel requests
        return UInt64(task.taskIdentifier)
    }

    ...

Camera

Manipulate the camera

In v10, the map’s camera is handled by the CameraManager. The MapView has a reference to the cameraManager.

CameraOptions is the v10 equivalent to pre-v10's MGLMapCamera. The map’s CameraOptions contain the center coordinate, padding, anchor, zoom, bearing, and pitch for a map. The camera options can be configured either as a whole or individually. The map’s camera can be configured via programmatic or gesture-based events. CameraOptions are different from the MapCameraOptions struct, which is used to configure and limit the camera’s behavior.

Set the map’s camera

The camera manager can be configured only after its parent MapView has been initialized. To set the map’s camera, first create a CameraOptions object, then direct the map to use it via the cameraManager. CameraOptions parameters are optional, so only the required properties need to be set.

pre-v10:

let centerCoordinate = CLLocationCoordinate2D(latitude: 21.3069,
                                              longitude: -157.8583)

mapView.setCenter(centerCoordinate, zoomLevel: 14, direction: 0, animated: false)

v10:

// Set the center coordinate of the map to Honolulu, Hawaii
let centerCoordinate = CLLocationCoordinate2D(latitude: 21.3069,
                                                    longitude: -157.8583)
// Create a camera
let camera = CameraOptions(center: centerCoordinate,
                             zoom: 14)

// Set the map's camera via the CameraManager
mapView.cameraManager.setCamera(to: camera)

You can set the map’s zoom level, pitch, and center coordinate programmatically using their respective CameraManager.setCamera* methods. Camera values should be set using the camera manager although they can be accessed as read-only properties on the MapView (for example, MapView.centerCoordinate).

Fit the camera to a given shape

In the Maps SDK v10, the approach to fitting the camera to a given shape like that of pre-v10 versions.

In v10, call camera:fitting on the CameraManager to create a camera for a given geometry, camera:coordinateBounds to create a camera that fits a set of rectangular coordinate bounds, or camera:coordinates to create a camera based off of a collection of coordinates. Then, call setCamera:camera on the CameraManager to visibly transition to the new camera.

Below is an example of setting the camera to a set of coordinates:

pre-v10:

// Fitting a camera to a set of coordinate bounds
let sw = CLLocationCoordinate2D(latitude: 24, longitude: -89)
let ne = CLLocationCoordinate2D(latitude: 26, longitude: -88)
let coordinateBounds = MGLCoordinateBounds(sw: sw, ne: ne)
let camera = mapView.cameraThatFitsCoordinateBounds(coordinateBounds)
mapView.setCamera(camera, animated: true)

v10:

let coordinates = [
    CLLocationCoordinate2DMake(24, -89),
    CLLocationCoordinate2DMake(24, -88),
    CLLocationCoordinate2DMake(26, -88),
    CLLocationCoordinate2DMake(26, -89),
    CLLocationCoordinate2DMake(24, -89)
]
let camera = mapView.cameraManager.camera(for: coordinates)
mapView.cameraManager.setCamera(to: camera)

Configure the default behavior of the camera

pre-v10

In pre-v10 versions of the Maps SDK, default camera behavior was configured directly on the map view using MGLMapView properties like minimumZoomLevel and minimumPitch. To limit the camera to a specific area you would use the mapView:shouldChangeFromCamera:toCamera: method on MGLMapViewDelegate as shown in the Restrict map panning to an area example.

v10:

In v10, there is a new MapCameraOptions struct that allows configuration of default behaviors for the camera positions, with the options outlined below:

public struct MapCameraOptions : Equatable {
    public var minimumZoomLevel: CGFloat
    public var maximumZoomLevel: CGFloat
    public var minimumPitch: CGFloat
    public var maximumPitch: CGFloat
    public var animationDuration: TimeInterval
    public var decelerationRate: CGFloat
    public var restrictedCoordinateBounds: CoordinateBounds?
}

The above struct can be customized and passed into the MapView through the same closure-based mechanism referenced earlier:

let sw = CLLocationCoordinate2DMake(-12, -46)
let ne = CLLocationCoordinate2DMake(2, 43)
let restrictedBounds = CoordinateBounds(southwest: sw, northeast: ne)

mapView.update { ( mapOptions ) in
        mapOptions.camera.minimumZoomLevel = 8.0
        mapOptions.camera.maximumZoomLevel = 15.0
        mapOptions.camera.restrictedCoordinateBounds = restrictedBounds
    }
}

The biggest change to this approach is how you restrict the camera to a given set of coordinate bounds, which you can now do using a simpler property-based approach instead of using the delegate method outlined earlier.

Map styles

Style Specification

In v10 the style API is aligned with the Mapbox Style Specification. Sources, Layers, and Light all work in the exact same manner as in the Style Specification.

The Style object in the MapView is responsible for all functionality related to runtime styling.

Note

Add sources and layers to the map only after it has finished loading its initial style. Use the lifecycle callbacks to be informed of when the style has finished loading.

Type-safe API

Every Source and Layer declared in the Style Specification exists in v10 as an exactly-typed Swift struct. Every property within those sources and layers is modeled using familiar Swift types. This means that creating new sources and layers should feel familiar to iOS developers who write in Swift.

Example: GeoJSON Source

For example, a minimal GeoJSON source can be created using the following code:

var myGeoJSONSource = GeoJSONSource()
myGeoJSONSource.maxZoom = 14.0

GeoJSON sources have a data property that you can set to either a URL or inline GeoJSON. The Swift struct representing a GeoJSON source is modeled 1:1 with this expectation.

The SDK uses turf-swift to model GeoJSON. This means that crafting GeoJSON at runtime is both straightforward and backed by the type-safety and codable conformance that Turf provides.

// Setting the `data` property with a url pointing to a GeoJSON document
myGeoJSONSource.data = .url(URL(string: "<path-to-geojson-file>"))

// Setting a Turf feature to the `data` property
myGeoJSONSource.data = .featureCollection(someTurfFeatureCollection)

The MapView holds a style object that can be used to add, remove, or update sources and layers.

self.mapView.style.addSource(myGeoJSONSource)

pre-v10:

This functionality previously required creation of an MGLShapeSource:

let shapeSource = MGLShapeSource(identifier:"my-geojson", url: URL(string: "<path-to-geojson-file>"), options: nil)

self.mapView.addSource(shapeSource)
Note

Updating GeoJSON sources happens differently in v10. You’ll need to call style.updateGeoJSON(for:with:) to update the data belonging to a GeoJSONSource.

Example: Background layer

Adding a background layer further demonstrates the type-safety provided in v10.

As mentioned earlier, all Layers are also Swift structs. The following code sets up a background layer and sets its background color to red:

var myBackgroundLayer = BackgroundLayer(id: "my-background-layer")
myBackgroundLayer.paint?.backgroundColor = .constant(ColorRepresentable(color: .red))

Once a layer is created, add it to the map:

self.mapView.style.addLayer(myBackgroundLayer)

Expressions

In the background layer example above, the backgroundColor property of the layer is set to a constant value. However, the backgroundColor property (and all layout and paint properties) also support expressions.

In v10, expressions have been redesigned from the ground up. The new Expression domain-specific language (DSL) directly models expressions based on the Mapbox Style Specification and removes the dependency on NSExpression.

In v10, expressions exist as familiar and type-safe Swift structs. They are also backed by Swift function builders to make the experience of writing an expression like the way SwiftUI works.

Consider an interpolate expression written in raw JSON:

[
  "interpolate",
  ["linear"],
  ["zoom"],
  0,
  "hsl(0, 79%, 53%)",
  14,
  "hsl(233, 80%, 47%)"
]

If this expression is applied to the backgroundColor paint property of a layer (the "land" layer, in the example screenshot above), then the backgroundColor would be interpolated from red to blue based on the zoom level of the map's camera.

pre-v10:

In pre-v10 versions of the Maps SDK, the expression would be written like this:

let colorStops: [NSNumber: UIColor] = [
    0: .red,
    14: .blue
];

layer.backgroundColor = NSExpression(format:
        "mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)",
        colorStops];

v10:

In the Maps SDK v10, a JSON expression from Mapbox Studio can be used directly or can be translated to Swift.

To use the JSON expression directly:

let expressionString =
"""
 [
   "interpolate",
   ["linear"],
   ["zoom"],
   0,
   "hsl(0, 79%, 53%)",
   14,
   "hsl(233, 80%, 47%)"
 ]
"""

if let expressionData = expressionString.data(using: .utf8) {
    let expJSONObject = try? JSONSerialization.jsonObject(with: expressionData, options: [])

    mapView.__map.setStyleLayerPropertyForLayerId("land",
                                                  property: "background-color",
                                                  value: expJSONObject)
}

To use translate the same expression to Swift:

Exp(.interpolate) {
    Exp(.linear)
    Exp(.zoom)
    0
    UIColor.red
    14
    UIColor.blue
}

Use the full power of Swift and the iOS runtime when defining this expression. For example, to adjust this expression based on whether the user has dark mode enabled:

var isDarkModeEnabled = traitCollection.userInterfaceStyle == .dark ? true : false
let lowZoomColor = isDarkModeEnabled? UIColor.black.expression : UIColor.red
let highZoomColor = isDarkModeEnabled? UIColor.grey.expression : UIColor.blue
Exp(.interpolate) {
    Exp(.linear)
    Exp(.zoom)
    0
    lowZoomColor
    14
    highZoomColor
}

Compose expressions

In v10, expressions can also be composed and built in a modular way. They can be infinitely nested or defined separately and added to the stack.

Exp(.interpolate) {
    Exp(.linear)
    Exp(.zoom)
    Exp(.subtract) {
        10
        3
    }
    UIColor.red
    Exp(.sum) {
        7
        7
    }
    UIColor.blue
}

Annotations

Maps can be marked with point, line, and polygon shapes using APIs associated with the MapView’s AnnotationManager.

Annotations were also supported pre-v10 by way of MGLShape objects that could conform to the MGLAnnotation protocol. For example, adding a single point annotation that marks a coordinate with an image previously looked like this:

pre-v10

class ViewController: UIViewController, MGLMapViewDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
      
  ...
        
        mapView.delegate = self

        let pointAnnotation = MGLPointAnnotation()
        pointAnnotation.coordinate = CLLocationCoordinate2D(latitude: 43, longitude: 10)
        mapView.addAnnotation(pisa)
    }

    func mapView(_ mapView: MGLMapView, imageFor annotation: MGLAnnotation) -> MGLAnnotationImage? {
        var customImage = UIImage(named: "star")!
        return MGLAnnotationImage(image: customImage, reuseIdentifier: "star")
    }
}

In the Maps SDK v10, the delegate-based approach is no longer used, moving in favor of custom Swift structs to represent annotations that can be customized with a property-based approach.

The largest change to annotations is that they are all powered internally by style layers. As a result, annotations should only be created after the map has finished loading its style to make sure the layers associated with the map have access to the style object.

See code examples for implementing point, line, and polygon annotations in Maps SDK v10 below.

Point annotations

A point annotation is a marker that is placed at a developer-specified coordinate:

let pointAnnotation = PointAnnotation(coordinate: customCoordinate)
mapView.annotationManager.addAnnotation(pointAnnotation)

This results in a marker at the customCoordinate specified. Remember, you must wait to make this call until after the map's style has finished loading.

Once an annotation has been placed it can be moved around the map with a call to updateAnnotation(_ annotation:) on the AnnotationManager:

pointAnnotation.coordinate = CLLocationCoordinate2DMake(0,0) mapView.annotationManager.updateAnnotation(pointAnnotation)

When adding a point annotation, the default annotation is a red marker pin. In addition, a custom image can be supplied to be used with a point annotation:

let image = UIImage(named: "star")
let pointAnnotation = PointAnnotation(coordinate: customCoordinate, image: customImage)

Line annotations

A line annotation connects a list of coordinates on the map with a line. Similar to the PointAnnotation example, it is created with a call to AnnotationManager:

// Line from New York City, NY to Washington, D.C.
let lineCoordinates = [
    CLLocationCoordinate2DMake(40.7128, -74.0060),
    CLLocationCoordinate2DMake(38.9072, -77.0369)
]

// Create the line annotation.
let lineAnnotation = LineAnnotation(coordinates: lineCoordinates)

// Add the annotation to the map.
mapView.annotationManager.addAnnotation(lineAnnotation)

Polygon annotations

The AnnotationManager can also add a polygon to the map by taking a list of coordinates that it will then attempt to connect. You can also create a polygon with an empty space in the middle (like a doughnut) by taking a separate list of coordinates. The order of the coordinates in the list matter and should conform to the GeoJSON specification.

let polygonCoords = [
    CLLocationCoordinate2DMake(24.51713945052515, -89.857177734375),
    CLLocationCoordinate2DMake(24.51713945052515, -87.967529296875),
    CLLocationCoordinate2DMake(26.244156283890756, -87.967529296875),
    CLLocationCoordinate2DMake(26.244156283890756, -89.857177734375),
    CLLocationCoordinate2DMake(24.51713945052515, -89.857177734375)
]

// This polygon has an interior polygon which represents a hole in the shape.
let polygonHole = [
    CLLocationCoordinate2DMake(25.085598897064752, -89.20898437499999),
    CLLocationCoordinate2DMake(25.085598897064752, -88.61572265625),
    CLLocationCoordinate2DMake(25.720735134412106, -88.61572265625),
    CLLocationCoordinate2DMake(25.720735134412106, -89.20898437499999),
    CLLocationCoordinate2DMake(25.085598897064752, -89.20898437499999)
]

// Create the polygon annotation.
let polygon = PolygonAnnotation(coordinates: polygonCoords, interiorPolygons: [polygonHole])

// Add the annotation to the map.
mapView.annotationManager.addAnnotation(polygon)

Selecting annotations

Annotations can also be selected programmatically or via a tap gesture. Ensuring that a delegate conforms to AnnotationInteractionDelegate enables selection and deselection events:

class MyViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        mapView.on(.mapLoadingFinished) { (event) in
            mapView.annotationManager.interactionDelegate = self
            let coordinate = CLLocationCoordinate2DMake(24, -89)
            let pointAnnotation = PointAnnotation(coordinate: coordinate)
            mapView.annotationManager.addAnnotation(pointAnnotation)
        }
    }
}

extension ViewController: AnnotationInteractionDelegate {
    public func didSelectAnnotation(annotation: Annotation) {
        print("Annotation selected")
    }

    public func didDeselectAnnotation(annotation: Annotation) {
        print("Annotation deselected")
    }
}

In v10, the AnnotationInteractionDelegate methods are similar to the annotation delegate methods of MGLMapViewDelegate in pre-v10 versions of the Maps SDK.

To select an annotation programmatically, call selectAnnotation methods on the MapView’s AnnotationManager.

To disable annotation selection, you can toggle the AnnotationManager.userInteractionEnabled property. By default, this is set to true.

View-based annotations are not supported. This means all annotations must be represented with either a static image with the PointAnnotation(coordinate:image:) initializer or the default red pin will be used.

Drag and drop support is not supported. However, this can be implemented with an advanced usage of various style APIs to update the data source of a GeoJSONSource after responding to a UIGestureRecognizer.

User location

The User Location component provides a mechanism by which the device’s current location can be observed and responded to. It also handles permissions and privacy, including precise vs. approximate location (which is new in iOS 14).

Requesting permissions

Users must grant permission before an iOS app can access information about their location. During this permission prompt, a custom string may be presented explaining how location will be used. This is specified by adding the key “NSLocationWhenInUseUsageDescription“ to the Info.plist file with a value that describes why the application is requesting these permissions.

<key>NSLocationWhenInUseUsageDescription</key>
<string>Your location is used to improve the map experience.</string>

Enabling location

Display the user's location with the following code:

mapView.update{ (mapOptions) in
   mapOptions.location.showUserLocation = true
}

The above code will enable rendering of a puck that represents the device's current location. It will also handle requesting permissions and all the nuances that come with different permission levels.

Custom Location Provider

The location framework has its own default Location Provider which will handle location updates, permissions, status, and more. But, a custom Location Provider can be used instead.

A Location Provider is a protocol, encapsulating functions and properties that will handle location updates, permission changes, and more. It has a delegate pattern so it will notify the delegate when changes in location occur. This like MGLLocationManager in pre-v10 versions of the Maps SDK.

pre-v10

In pre-v10 versions of the Maps SDK, a custom Location Provider could be provided by making a custom MGLLocationManager object:

class CustomLocationManager: NSObject, MGLLocationManager {
    // Implement protocol required functions
}

self.mapView.locationManager = CustomLocationManager()

v10:

A custom Location Provider must conform to the LocationProvider protocol and implement the required functions from the protocol. The custom provider can be passed to the LocationManager with the following code:

mapView.locationManager.overrideLocationProvider(with: customLocationProvider)

Handling privacy

In iOS 14, Apple introduced a new layer of privacy protection to location. There is now a new property called accuracyAuthorization. This allows the user to select between precise and approximate mode for their location tracking. When precise mode is enabled, the code will function as it did in previous iOS versions. But, in approximate mode, the device will send location updates less often and will specify a large radius in which the device is located rather than a precise location.

If a user decides to turn off precise location and deny temporary full accuracy, then it will show an approximate ring, which looks like this:

In order to handle a change in location privacy, there are two options: let the Maps SDK handle it or implement the delegate.

Let the SDK handle changes in location privacy

The Maps SDK's default implementation will listen for a change of accuracy authorization. If the user has decided to turn off their precise location, then in the next app session, the user will be prompted with an alert to request a temporary full accuracy authorization. You can set this functionality and the contents of the dialogue in the app’s Info.plist file. Add the key “NSLocationTemporaryUsageDescriptionDictionary“ and add the following key-value pair:

  • Key: “LocationAccuracyAuthorizationDescription“
  • Value: A sentence describing why the application needs this precise location
<key>NSLocationTemporaryUsageDescriptionDictionary</key>
<dict>
    <key>LocationAccuracyAuthorizationDescription</key>
    <string>This application temporarily requires your precise location.</string>
</dict>

Implement the location permissions delegate

The LocationPermissionsDelegate protocol has a set of delegate methods that will be called when the user makes changes to location permissions.

class ViewController: UIViewController {
    // This controller's initialization has been omitted in this code snippet
    var mapView: MapView! 

    override func viewDidLoad() {
        super.viewDidLoad()
        mapView.locationManager.delegate = self
    }

    // Selector that will be called as a result of the delegate below
    func requestPermissionsButtonTapped() {
        mapView.locationManager.requestTemporaryFullAccuracyPermissions(withPurposeKey: "customKey")
    }
}

extension ViewController: LocationPermissionsDelegate {
    func locationManager(_ locationManager: LocationManager, didChangeAccuracyAuthorization accuracyAuthorization: CLAccuracyAuthorization) {
        if accuracyAuthorization == .reducedAccuracy {
         // Present a button on screen that asks for full accuracy
         // This button can have a selector as defined above
        }
    }
}

This code snippet also has our recommendation for how to handle a location permission change. A withPurposeKey parameter is specified. This value must correspond to the key of a key-value pair that was specified in Info.plist.

<key>NSLocationTemporaryUsageDescriptionDictionary</key>
<dict>
    <key>CustomKey<key>
    <string>We temporarily require your precise location for an optimal experience.</string>
</dict>

New features and improvements

Platform-driven camera animation system

The Maps SDK v10 introduces a new camera animation system which leverages Core Animation.

Familiar Core Animation constructs that come packaged with UIKit can be used to create animated transitions from one map perspective to another.

In v10 this will simplify complex implementations such as an animation that performs two transitions at the same time:

  1. The camera should zoom in from a '0' zoom level to a '7' zoom level.
  2. Rotate the map from 0 degrees to 180 degrees.

The Maps SDK v10 also provides fine-grained control over the transition interval (5 seconds).

In v10, this requires a call to the familiar animate static function on UIView:

UIView.animate(withDuration: 5.0) {
    mapView.cameraView.zoom = 7.0
    mapView.cameraView.bearing = 180.0
}

Keyframe animations and driving animations with UIViewPropertyAnimator are not supported.

3D model capabilities

The Maps SDK v10 introduces a new 3D model layer. This new layer will allow for 3D assets in the glTF format to be used, for example as a custom user indicator.

The ModelLayer is linked to a ModelSource. The ModelSource drives the positioning and orientation of a 3D glTF asset present in the application bundle. The ModelLayer handles the rendering of the asset.

The example below renders a map with a 3D asset placed at the specified coordinate.

public class ModelExample: UIViewController {

    internal var mapView: MapView!

    override public func viewDidLoad() {
        super.viewDidLoad()

        self.mapView = MapView(with: view.bounds, resourceOptions: resourceOptions())
        self.view.addSubview(mapView)

        self.mapView.on(.styleLoadingFinished) { [weak self] _ in
            guard let self = self else { return }
            let uri = Bundle.main.url(forResource: "race_car_model",
                                      withExtension: "gltf")

            let raceCarModel = Model(uri: uri,
                                     position: [-122.4194, 37.7749],
                                     orientation: [0, 0, 90.0])

            var modelSource = ModelSource()
            modelSource.models = ["race-car": raceCarModel]
            mapView.style.addSource(source: modelSource,
                                    identifier: "race-car-model-source")

            var modelLayer = ModelLayer(id: "race-car-model-layer")
            modelLayer.layout?.visibility = .visible
            modelLayer.paint?.modelOpacity = .constant(0.8)
            modelLayer.paint?.modelScale = [.constant(5.0), .constant(5.0), .constant(5.0)]
            modelLayer.source = "race-car-model-source"

            mapView.style.addLayer(layer: modelLayer)

            var skyLayer = SkyLayer(id: "my-sky")
            skyLayer.paint?.skyType = .atmosphere

            mapView.style.addLayer(layer: skyLayer)

            let coordinate = CLLocationCoordinate2D(latitude: 37.7749,
                                                      longitude: -122.4194)
            self.mapView.cameraManager.setCamera(centerCoordinate: coordinate,
                                                 zoom: 19,
                                                 pitch: 80)
        }
    }

3D terrain and sky layers

In the Maps SDK v10, you can show dramatic elevation changes against an atmospheric backdrop by enabling 3D terrain and using the new sky layer.

To configure this, first add a RasterDemSource to a map style, with a URL source pointed to Mapbox's global digital elevation model (DEM), Mapbox Terrain RGB. Then, create a Terrain object and define the exaggeration factor, which acts as a scale to represent extrusion. The higher the value, the more exaggerated land features at higher elevations will appear. Adding a new sky layer gives the appearance of the sun illuminating land, especially when the camera’s pitch is changed.

To enable 3D terrain and add a new sky layer:

// Add terrain
var demSource = RasterDemSource()
demSource.url = "mapbox://mapbox.mapbox-terrain-dem-v1"
demSource.tileSize = 512
demSource.maxzoom = 14.0
_ = self.mapView.style.addSource(source: demSource, identifier: "mapbox-dem")

var terrain = Terrain(sourceId: "mapbox-dem")
terrain.exaggeration = .constant(1.5)

// Add sky layer
_ = self.mapView.style.setTerrain(terrain)

var skyLayer = SkyLayer(id: "sky-layer")
skyLayer.paint?.skyType = .atmosphere
skyLayer.paint?.skyAtmosphereSun = .constant([0.0, 0.0])
skyLayer.paint?.skyAtmosphereSunIntensity = .constant(15.0)

_ = self.mapView.style.addLayer(layer: skyLayer)
Note

3D terrain and sky layers are still in an experimental state. It might not work as expected with the map camera animation system.

Custom rendered layers

For more advanced rendering operations, the Maps SDK v10 supports inserting custom Metal layers within the map’s style. This unlocks many new opportunities for customization and is specifically tailored to those looking to add their own custom rendering code. For an implementation demo, see the “Add a custom rendered layer” example within the Examples application for more details.

Cache manager

CacheManager is an interface for managing the device's ambient cache, used for storing tile data. It allows developers to reset, clear, reconfigure, and pre-load data. This class exposes similar cache management as MGLOfflineStorage did in pre-v10 versions of the Maps SDK.

pre-v10:

[[MGLOfflineStorage sharedOfflineStorage] invalidateAmbientCacheWithCompletionHandler:^{NSError *error) {
    // Business logic
}

v10:

// Default cache size and paths
let resourceOptions = ResourceOptions(accessToken: accessToken)
let cacheManager = CacheManager(options: resourceOptions)

cacheManager.invalidateAmbientCache { _ in 
    // Business logic
}

Deprecations and removals

  • Expression support using NSExpression has been removed.
  • OpenGL map rendering has been removed in favor of Metal.
  • Storing your access token in the application's Info.plist is discouraged.
  • OfflineManager has been deprecated and renamed to OfflineRegionManager.