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
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 ResourceOptionsManager
s as you need them. This can be useful if you need a mechanism to share ResourceOptions
between parts of your application.
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.
Function | Description |
---|---|
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)