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

Starting with v10.0.0-beta.15, building with Apple Silicon is supported.

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!
    var completion: (() -> Void)?

    override func viewDidLoad() {
        super.viewDidLoad()
        
        CredentialsManager.default.accessToken = "YOUR_ACCESS_TOKEN"

        mapView = MapView(frame: view.bounds)
        view.addSubview(mapView)
        completion?()
    }
}

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!
    var handler: ((Event) -> Void)?

    override func viewDidLoad() {
        super.viewDidLoad()
        mapView = MapView(frame: view.bounds)
        view.addSubview(mapView)

        CredentialsManager.default.accessToken = accessToken

        /**
         The closure is called when style data has been loaded. This is called
         multiple times. Use the event data to determine what kind of style data
         has been loaded.

         When the type is `style` this event most closely matches
         `-[MGLMapViewDelegate mapView:didFinishLoadingStyle:]` in SDK versions
         prior to v10.
         */
        mapView.on(.styleDataLoaded) { [weak self] (event) in
            guard let data = event.data as? [String: Any],
                  let type = data["type"],
                  let handler = self?.handler else {
                return
            }

            print("The map has finished loading style data of type = \(type)")
            handler(event)
        }

        /**
         The closure is called during the initialization of the map view and
         after any `styleDataLoaded` events; it is called when the requested
         style has been fully loaded, including the style, specified sprite and
         source metadata.

         This event is the last 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.

         Changes to sources or layers of the current style do not cause this
         event to be emitted.
         */
        mapView.on(.styleLoaded) { [weak self] (event) in
            print("The map has finished loading style ... Event = \(event)")
            self?.handler?(event)
        }

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

         This is an ideal time 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(.mapLoaded) { [weak self] (event) in
            print("The map has finished loading... Event = \(event)")
            self?.handler?(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 been rendered
         - All fade/transition animations have completed
         */
        mapView.on(.mapIdle) { [weak self] (event) in
            print("The map is idle... Event = \(event)")
            self?.handler?(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 associated error message to notify the user that map
         data is unavailable.
         */
        mapView.on(.mapLoadingError) { (event) in
            guard let data = event.data as? [String: Any],
                  let type = data["type"],
                  let message = data["message"] else {
                return
            }

            print("The map failed to load.. \(type) = \(message)")
        }
    }
}

It's worth noting that the closest equivalent of pre-v10's -[MGLMapViewDelegate mapView:didFinishLoadingStyle:] is the styleDataLoaded event when its associated type is style, though we recommended using styleLoaded instead.

The following simplified diagram helps explain the event lifecycle:


 ┌─────────────┐               ┌─────────┐                   ┌──────────────┐
 │ Application │                  Map                      │ResourceLoader│
 └──────┬──────┘               └────┬────┘                   └───────┬──────┘
                                                                   
        ├───── Set style URL ──────▶│                                
                                   ├───────────get style───────────▶│
                                                                   
                                   │◀─────────style data────────────┤
                                                                   
                                   ├─parse style─┐                  
                                                                  
              styleDataLoaded      ◀─────────────┘                  
        │◀────{"type": "style"}─────┤                                
                                   ├─────────get sprite────────────▶│
                                                                   
                                   │◀────────sprite data────────────┤
                                                                   
                                   ├──────parse sprite───────┐      
                                                                  
              styleDataLoaded      ◀─────────────────────────┘      
        │◀───{"type": "sprite"}─────┤                                
                                   ├─────get source TileJSON(s)────▶│
                                                                   
             sourceDataLoaded      │◀─────parse TileJSON data───────┤
        │◀──{"type": "metadata"}────┤                                
                                                                   
                                                                   
              styleDataLoaded                                      
        │◀───{"type": "sources"}────┤                                
                                   ├──────────get tiles────────────▶│
                                                                   
        │◀───────styleLoaded────────┤                                
                                                                   
             sourceDataLoaded      │◀─────────tile data─────────────┤
        │◀────{"type": "tile"}──────┤                                
                                                                   
                                                                   
        │◀────renderFrameStarted────┤                                
                                   ├─────render─────┐               
                                                                  
                                   ◀────────────────┘               
        │◀───renderFrameFinished────┤                                
                                   ├──render, all tiles loaded──┐   
                                                                  
                                   ◀────────────────────────────┘   
        │◀────────mapLoaded─────────┤                                
                                                                   
                                                                   
        │◀─────────mapIdle──────────┤                                
                            ┌ ─── ─┴─ ─── ┐                         
                               offline                            
                            └ ─── ─┬─ ─── ┘                         
                                                                   
        ├──────── Set camera ──────▶│                                
                                   ├───────────get tiles───────────▶│
                                                                   
                                   │┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   
        │◀─────────mapIdle──────────┤   waiting for connectivity    
                                   ││  Map renders cached data      
                                   │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘  
                                                                   

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.

// Configure map to show a scale bar
mapView.ornaments.options.scaleBar.visibility = .visible

mapView.update { (mapOptions) in
    // 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 {
        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 camera.

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. The current camera options can be accessed via the map's cameraOptions property.

Set the map’s camera

The camera manager can be configured with an initial camera view. To set the map’s camera, first create a CameraOptionsobject, then direct the map to use it via the MapInitOptions.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)

let options = MapInitOptions(cameraOptions: camera)
let mapView = MapView(frame: frame, mapInitOptions: options)

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(for:) functions on the MapboxMap to create: a camera for a given geometry, a camera that fits a set of rectangular coordinate bounds or a camera based off of a collection of coordinates. Then, call ease(to:) on MapView.camera 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.mapboxMap.camera(for: coordinates,
                                      padding: .zero,
                                      bearing: nil,
                                      pitch: nil)
mapView.camera.ease(to: camera, duration: 10.0)

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

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(someGeoJSONDocumentURL)

// 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.

try mapView.style.addSource(myGeoJSONSource, id: "my-geojson-source")

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:

try 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.mapboxMap.__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.annotations.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.annotations.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.annotations.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.annotations.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.annotations.interactionDelegate = self
            let coordinate = CLLocationCoordinate2DMake(24, -89)
            let pointAnnotation = PointAnnotation(coordinate: coordinate)
            mapView.annotations.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.puckType = .puck2D()
}

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.location.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.location.delegate = self
    }

    // Selector that will be called as a result of the delegate below
    func requestPermissionsButtonTapped() {
        mapView.location.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 CoreAnimation. Along with providing familiar high-level animations such as easeTo or flyTo, the SDK provides expressive new ways of controlling animations with more granularity. Support for a variety of animation curves that a transition could follow is included in the Maps SDK v10.

For example, consider a situation that necessitates zooming into the map while also rotating the map. The mapView.camera exposes a set of powerful new makeAnimator* functions that allow for building this in an expressive manner:

lazy var myCustomAnimator: BasicCameraAnimator = {
    mapView.camera.makeAnimator(duration: 5, curve: .easeInOut) { (transition) in
        
        // Transition the zoom level of the map's camera to `7`
        transition.zoom.toValue = 7
        
        // Transition the bearing of the map's camera to `180` degrees
        transition.bearing.toValue = 180
    }
}()

.... 
....
// Call `startAnimation()` to start the animation (at some point after the `.mapLoaded` event)
myCustomAnimator.startAnimation()

This animation will result in the following changes to the map's camera:

  1. The map's zoom will animate to a zoom level of 7 from the value it was before startAnimation() was called.
  2. The map's bearing will animate to 180 degrees from the value it was before startAnimation() is called.
  3. The entire animation will last for 5 seconds and will interpolate with a .easeInOut curve.

Control camera transitions with more granularity

The transition construct passed into every "animation" block allows you to control both where the animation completes (in other words, the final map camera) and its starting position. The snippet below illustrates this:

lazy var myCustomAnimator: BasicCameraAnimator = {
    mapView.camera.makeAnimator(duration: 5, curve: .easeInOut) { (transition) in
        
        // Transition the zoom level of the map's camera from `10` to `7`
    transition.zoom.fromValue = 10
        transition.zoom.toValue = 7
        
        // Transition the bearing of the map's camera from `45` degrees to `180` degrees
    transition.bearing.fromValue = 45
        transition.bearing.toValue = 180
    }
}()

.... 
....
// Call `startAnimation()` to start the animation (at some point after the `.mapLoaded` event)
myCustomAnimator.startAnimation()

This animation will result in the following changes to the map's camera:

  1. First, the map's zoom will change to 10 and the bearing will change to 45 without an animated transition.
  2. The map's zoom will then animate to a zoom level of 7 from an initial value of 10.
  3. The map's bearing will animate to 180 degrees from an initial value of 45 degrees.
  4. The entire animation will last for 5 seconds and will interpolate with a .easeInOut curve.

Chain animations

BasicCameraAnimator also supports completion blocks with the following API:

myCustomAnimator.addCompletion { position in 
    print("Animation complete at position: \(position)")
}

These completion blocks can be used to chain animations to execute one after another, as in this example:

// Store the CameraAnimators so they don't fall out of scope
lazy var zoomAnimator: BasicCameraAnimator = {
    let animator = mapView.camera.makeAnimator(duration: 4, curve: .easeInOut) { (transition) in
        transition.zoom.toValue = 14
    }

    animator.addCompletion { [unowned self] (_) in
        print("Animating camera pitch from 0 degrees -> 55 degrees")
        self.pitchAnimator.startAnimation()
    }

    return animator
}()

lazy var pitchAnimator: BasicCameraAnimator = {
    let animator = mapView.camera.makeAnimator(duration: 2, curve: .easeInOut) { (transition) in
        transition.pitch.toValue = 55
    }

    animator.addCompletion { [unowned self] (_) in
        print("Animating camera bearing from 0 degrees -> 45 degrees")
        self.bearingAnimator.startAnimation()
    }

    return animator
}()

lazy var bearingAnimator: BasicCameraAnimator = {
    let animator = mapView.camera.makeAnimator(duration: 4, curve: .easeInOut) { (transition) in
        transition.bearing.toValue = -45
    }

    animator.addCompletion { (_) in
        print("All animations complete!")
    }

    return animator
}()

// Start the zoomAnimator at some point after the `.mapLoaded` event
zoomAnimation.startAnimation()

AnimationOwner

AnimationOwner is an Enum used to keep track the source (or owner) for each animation. AnimationOwner has the following values:

  • gestures: support for animations run by gestures.
  • unspecified: support for a non-specific animation.
  • custom(id: String): support for your own custom owner.

Notes on camera animation

  • A BasicCameraAnimator's lifecycle is important. If a BasicCameraAnimator is destroyed before an animation is complete, the animation will immediately stop.
  • CameraAnimators are created with a default owner of .unspecified.
  • The CameraManager will hold a list of all active animators.

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
try mapView.style.addSource(demSource, id: "mapbox-dem")

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

// Add sky layer
try mapView.style.setTerrain(terrain)

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

try mapView.style.addLayer(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
}

New OfflineManager

The Maps SDK v10 introduces a new OfflineManager API that manages style packs and produces tileset descriptors for the tile store.

  • By default, you may download up to 250 MB of data for offline use without incurring additional charges. This limit is subject to change during the beta.
  • The new API replaces the deprecated OfflineRegionManager API. The OfflineManager API can be used to create offline style packs that contain style data, such as: style definition, sprites, fonts and other resources. Tileset descriptors created by the OfflineManager API are used to create tile packs via TileStore API. Mobile maps SDKs use tile packs for rendering map content.

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.