Skip to main content

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 MapboxMap, Style, Camera and Location).

All capabilities related to the map 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.

ResourceOptions configuration (including access token)

In pre-v10 the access token was configured either using the Info.plist key MGLMapboxAccessToken or by programmatically setting MGLAccountManager.accessToken.

In v10, a new ResourceOptionsManager has been introduced to manage the application-scoped ResourceOptions, which includes an access token.

The shared ResourceOptionsManager.default is an application wide convenience. It's used by default by MapInitOptions (and in turn by MapView.init()) if you do not provide a custom ResourceOptions. You should use this convenience to set an application wide access token:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
ResourceOptionsManager.default.resourceOptions.accessToken = "my-application-access-token"
return true
}

If you choose not to set the token programmatically, the shared instance will look in the application's Info.plist for the value associated with the MBXAccessToken key.

You can create ResourceOptionsManagers as you need them. This can be useful if you need a mechanism to share ResourceOptions between parts of your application.

Note

If you are working with Storyboards or Xibs, ResourceOptionsManager.default will be used when creating the required MapInitOptions. If you wish to provide a custom MapInitOptions (which includes the ResourceOptions), you can connect a custom type conforming to the MapInitOptionsProvider protocol to the mapInitOptionsProvider IBOutlet. This gives you a mechanism to override the access token used by a MapView.

Display a map

The primary interface in v10 is the MapView, which, like in earlier versions of the SDK, is a UIView.

MapboxMap has also been introduced, which grants access to map properties (as opposed to UIView associated properties). The MapView.mapboxMap property is the equivalent of Android's getMapboxMap() function.

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 accessToken: String!

override func viewDidLoad() {
super.viewDidLoad()

ResourceOptionsManager.default.resourceOptions.accessToken = self.accessToken

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

The result will look like this:

Style loading functions

You can separate style loading from map view creation using the new loadStyleURI and loadStyleJSON functions. These have convenient closures to return the loaded Style and any map loading error.

FunctionDescription
loadStyleURI(_:completion:)Load a new map style asynchronous from the specified URI.
loadStyleJSON(_:completion:)Load style from a JSON string.

Here's an example illustrating how to delay loading a map style using a style URL:

// Pass a nil StyleURI to stop the map from loading a default style
mapView = MapView(frame: bounds, mapInitOptions: MapInitOptions(styleURI: nil))

...

mapView.mapboxMap.loadStyleURI(.streets) { result in
switch result {
case .success(let style):
print("The map has finished loading the style")
// Do something with `style`

case let .failure(error):
print("The map failed to load the style: \(error)")
}
}

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

override func viewDidLoad() {
super.viewDidLoad()

ResourceOptionsManager.default.resourceOptions.accessToken = self.accessToken

mapView = MapView(frame: view.bounds)
view.addSubview(mapView)

/**
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.mapboxMap.onEvery(.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.mapboxMap.onNext(.styleLoaded) { (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.mapboxMap.onNext(.mapLoaded) { (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.mapboxMap.onNext(.mapIdle) { (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.mapboxMap.onNext(.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 the MapboxMap.loadStyleURI() function instead. loadStyleURI() listens for the .styleLoaded event.

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 default configurations for map elements such as gestures, ornaments, and the map's camera.

The map's configurations can be updated via the MapView and its mapboxMap property.

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;

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 restricted bounds are set directly via the MapView.mapboxMap.

let restrictedBounds = CoordinateBounds(southwest: CLLocationCoordinate2D(latitude: 10, longitude: 10),
northeast: CLLocationCoordinate2D(latitude: 11, longitude: 11))
let cameraBoundsOptions = CameraBoundsOptions(bounds: restrictedBounds)

// Configure map to show a scale bar
mapView.ornaments.options.scaleBar.visibility = .visible
try mapView.mapboxMap.setCameraBounds(with: cameraBoundsOptions)

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 making any API requests, 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 {
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

let result: Result<HttpResponseData, HttpRequestError>

if let error = error {
// Map NSURLError to HttpRequestErrorType
let requestError = HttpRequestError(type: .otherError, message: error.localizedDescription)
result = .failure(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)
result = .success(responseData)
} else {
// Error
let requestError = HttpRequestError(type: .otherError, message: "Invalid response")
result = .failure(requestError)
}

let response = HttpResponse(request: request, result: result)
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 CameraAnimationsManager. The MapView has a reference to the CameraAnimationsManager called 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. The current camera state of the map can be accessed via the mapView's cameraState 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 CameraOptions object, 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 by initializing a CameraOptions object with those values. You can the set the camera using mapView.mapboxMap.setCamera() Camera values should be set using the MapboxMap.setCamera() method, although they can be accessed as read-only properties on the map view's cameraState (for example, MapView.cameraState.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 are new CameraBounds and CameraBoundsOptions struct that allows configuration of default behaviors for the camera positions, with the options outlined below:

public struct CameraBoundsOptions: Hashable {

/// The latitude and longitude bounds to which the camera center are constrained.
public var bounds: CoordinateBounds?

/// The maximum zoom level, in mapbox zoom levels 0-25.5. At low zoom levels,
/// a small set of map tiles covers a large geographical area. At higher
/// zoom levels, a larger number of tiles cover a smaller geographical area.
public var maxZoom: CGFloat?

/// The minimum zoom level, in mapbox zoom levels 0-25.5.
public var minZoom: CGFloat?

/// The maximum allowed pitch value in degrees.
public var maxPitch: CGFloat?

/// The minimum allowed pitch value degrees.
public var minPitch: CGFloat?

...

Use the setCameraBounds(with:) function on MapView.mapboxMap to configure the above properties.

let sw = CLLocationCoordinate2DMake(-12, -46)
let ne = CLLocationCoordinate2DMake(2, 43)
let restrictedBounds = CoordinateBounds(southwest: sw, northeast: ne)
try mapView.mapboxMap.setCameraBounds(with: CameraBoundsOptions(bounds: restrictedBounds,
maxZoom: 15.0,
minZoom: 8.0))

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 function-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.mapboxMap handles 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 uncomplicated 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.mapboxMap holds a style object that can be used to add, remove, or update sources and layers.

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

pre-v10:

This functionality used to 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.updateGeoJSONSource(withId:geoJSON:) to update the data belonging to a GeoJSONSource.

Note

In v10, objects like MGLPointFeature, MGLPolylineFeature, MGLPolygonFeature and MGLShapeCollectionFeature are replaced by GeoJSONSource in conjunction with Turf’s Point, LineString, MultiLineString, Polygon, etc.

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.backgroundColor = .constant(StyleColor(.red))

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

try mapView.mapboxMap.style.addLayer(myBackgroundLayer)

Localization

v10 allows developers to change the language displayed by a map style based on a given locale. This ability can be enabled with the following code:

// Changes locale to Spanish
mapView.mapboxMap.style.localizeLabels(into: Locale(identifier: "en"))

There is also an opportunity to provide an array of layer ids so that only a subset of layers will be localized. This can be achieved with the following code:

// Changes all country labels to Japanese
mapView.mapboxMap.style.localizeLabels(into: Locale(identifier: "ja"), forLayerIds: ["country-label"])

Expressions

In the background layer example above, the backgroundColor property of the layer is set to a constant value. But 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, you would write the expression 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: [])

try mapView.mapboxMap.style.setLayerProperty(for: "land",
property: "background-color",
value: expJSONObject)
}

To translate the same JSON 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

You can add "annotations" to the map using point, line, polygon and circle shapes with the MapView’s AnnotationOrchestrator. Use the AnnotationOrchestrator to create annotation managers based on the type of annotation that you're interested in. Every annotation manager handles a collection of annotations. Once a manager has been created, you can create and add individually styled instances of the corresponding annotation type.

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 looked like this in previous versions:

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.

View annotations available in v10.2.0 and higher

In v6, view annotations were possible with the MGLAnnotationView class.In v10, view annotations are only available in v10.2.0 and higher. For more details see the View annotations guide.

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

Point annotations

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

// Make a `PointAnnotationManager` which will be responsible for managing a collection of `PointAnnotion`s.
let pointAnnotationManager = mapView.annotations.makePointAnnotationManager()

// Initialize a point annotation with a geometry ("coordinate" in this case)
// and configure it with a custom image (sourced from the asset catalogue)
var customPointAnnotation = PointAnnotation(coordinate: customCoordinate)

// Make the annotation show a red pin. See /ios/maps/examples/point-annotation/ for a complete example.
customPointAnnotation.image = .init(image: UIImage(named: "red_pin")!, name: "red_pin")

// Add the annotation to the manager in order to render it on the mao.
pointAnnotationManager.annotations = [customPointAnnotation]

This results in a marker at the customCoordinate specified.

The PointAnnotationManager created above also controls the lifecycle of the annotations in its purview. Annotation managers are kept alive by the AnnotationOrchestrator until they are removed explicitly via a call to removeAnnotationManager(withId:) or implicitly by creating another annotation manager with the same ID.

Line annotations

A line annotation connects a list of coordinates on the map with a line. Like the PointAnnotation example, you must create instances of LineAnnotations with some geometry and then add it to a LineAnnotationManager:

// 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.
var lineAnnotation = PolylineAnnotation(lineCoordinates: lineCoordinates)

// Customize the style of the line annotation
lineAnnotation.lineColor = StyleColor(.red)
lineAnnotation.lineOpacity = 0.8
lineAnnotation.lineWidth = 10.0

// Create the PolylineAnnotationManager responsible for managing
// this line annotations (and others if you so choose)
let lineAnnnotationManager = mapView.annotations.makePolylineAnnotationManager()

// Add the annotation to the manager.
lineAnnnotationManager.annotations = [lineAnnotation]

Polygon annotations

The AnnotationManager can also add a polygon to the map by taking a list of coordinates that it will then try 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.

// Describe the polygon's geometry
let outerRingCoords = [
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 intererior polygon which represents a hole in the shape.
let innerRingCoords = [
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 with the outer ring and inner ring
let outerRing = Turf.Ring(coordinates: outerRingCoords)
let innerRing = Turf.Ring(coordinates: innerRingCoords)

let polygon = Turf.Polygon(outerRing: outerRing, innerRings: [innerRing])

// Create the PolygonAnnotationManager
let polygonAnnotationManager = mapView.annotations.makePolygonAnnotationManager()

// Create the polygon annotation
var polygonAnnotation = PolygonAnnotation(polygon: makePolygon())

// Style the polygon annotation
polygonAnnotation.fillColor = StyleColor(.red)
polygonAnnotation.fillOpacity = 0.8

// Add the polygon annotation to the manager
polygonAnnotationManager.annotations = [polygonAnnotation]

Selecting annotations

Annotations can also be interacted with via a tap gesture. Ensuring that a delegate conforms to AnnotationInteractionDelegate enables tap events:

class MyViewController: UIViewController, AnnotationInteractionDelegate {
@IBOutlet var mapView: MapView!

override func viewDidLoad() {
super.viewDidLoad()

// Create the point annotation, which will be rendered with a red pin.
let coordinate = mapView.cameraState.center
var pointAnnotation = PointAnnotation(coordinate: coordinate)
pointAnnotation.image = .init(image: UIImage(named: "red_pin")!, name: "red_pin")

// Create the point annotation manager
let pointAnnotationManager = mapView.annotations.makePointAnnotationManager()

// Allow the view controller to accept annotation selection events.
pointAnnotationManager.delegate = self

// Add the annotation to the map.
pointAnnotationManager.annotations = [pointAnnotation]
}

// MARK: - AnnotationInteractionDelegate
public func annotationManager(_ manager: AnnotationManager,
didDetectTappedAnnotations annotations: [Annotation]) {
print("Annotations tapped: \(annotations)")
}
}

View-based annotations are not supported. All point annotations must be configured with a static image.

Drag and drop support is not supported. But 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 full vs. reduced accuracy (which was added 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>

Showing the device's location

Display the device's location with the following code:

mapView.location.options.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.

Following the device's location

v6 tracking modes have been replaced by the Viewport API (available starting in v10.3). Besides following objects on a map, it can also be extended with custom states and transitions.

For more details, see the User location guide.

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.

LocationProvider is a protocol, encapsulating functions and properties that will handle location updates, permission changes, and more. It notifies its delegate when changes in location, heading, authorization, or error status occur. This is 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 full accuracy and reduced accuracy modes for their location tracking. When full accuracy mode is enabled, the code will function as it did in previous iOS versions. But, in reduced accuracy 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 full accuracy location and deny temporary full accuracy, then the 2D puck will be rendered as an approximate ring, which looks like this:

To handle changes in location privacy, there are two options: let the Maps SDK handle the changes or implement the delegate.

Let the SDK handle changes in location privacy

The Maps SDK's default implementation will listen for a change in accuracy authorization. If the user has decided to turn off full accuracy 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 full accuracy locations
<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 the app's accuracy authorization.

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>

Offline

Retaining your offline cache from previous Maps SDK versions

Migrating from previous versions of the Maps SDK requires the following steps to make sure that resources created in v6 are available to users in v10 as the offline database has moved.

The offline database file, which was located at /Library/Application Support/<bundle id>/.mapbox/cache.db in v6, is now located at /Library/Application Support/.mapbox/map_data/map_data.db. The SDK will not automatically detect the legacy database and an application level migration is necessary to move the existing database.

Below is a sample method to do this migration:

func migrateOfflineCache() {

// Old and new cache file paths
let srcURL = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("/Library/Application Support/com.mapbox.examples/.mapbox/cache.db")

let destURL = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("/Library/Application Support/.mapbox/map_data/map_data.db")

let fileManager = FileManager.default

do {
try fileManager.createDirectory(at: destURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
try fileManager.moveItem(at: srcURL, to: destURL)
print("Move successful")
} catch {
print("Error: \(error.localizedDescription)")
}
}

Note that this method will need to be called before the new cache is created.

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 a struct used to keep track the source (or owner) for each animation. AnimationOwner has the following predefined values:

  • gestures: support for animations run by gestures.
  • unspecified: support for a non-specific animation.

Custom values may be created via AnimationOwner(rawValue:).

Notes on camera animation

  • A BasicCameraAnimator's lifecycle is important. If a BasicCameraAnimator is destroyed before an animation is complete, the animation will stop at once.
  • 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.mapboxMap.style.addSource(demSource, id: "mapbox-dem")

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

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

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

try mapView.mapboxMap.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.

New OfflineManager

v10 introduces a new OfflineManager API that manages style packs and produces tileset descriptors for the tile store. Read more about OfflineManager in the Offline guide.

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.
  • The legacy OfflineManager has been deprecated and renamed to OfflineRegionManager.
Was this page helpful?