View annotations

Add view annotations on top of the map view using the Mapbox Maps SDK's View Annotations API. View annotations are UIKit views that are drawn on top of a Mapbox MapView and bound to some Geometry. This can be helpful for achieving common patterns used in maps in mobile applications like displaying an information window when a POI (point of interest) is tapped.

Supported geometry types

Only Point geometry is supported for now. Supporting other geometry types will be added in upcoming releases.

Benefits:

  • Straight-forward API that allows adding an iOS UIView as a view annotation, making it possible to add clickable buttons or any other UI elements.
  • High rendering performance when using a reasonable number of views (generally below 100, but may vary based on the view's content and the user's device model).
  • Wide number of options including allowing overlap between view annotations, selected state, connecting to a map feature, and more.

Limitations:

  • Inefficient and poor performance when adding many features (> 250) to the map if allow overlap is turned on.

Create a view annotation

Create a UIView

Start by creating a new UIView or subclass of UIView that contains the contents of the view annotation. This can include anything from text to images to interactive elements and more.

For example, to create a minimal UIView containing text:

private func createSampleView(withText text: String) -> UIView {
    let label = UILabel()
    label.text = text
    label.font = .systemFont(ofSize: 14)
    label.numberOfLines = 0
    label.textColor = .black
    label.backgroundColor = .white
    label.textAlignment = .center
    return label
}

Add a view annotation to the MapView

To add the view annotation on top of the MapView, start by:

  1. Defining options for the view annotation.
  2. Creating the sample view from above.
  3. Passing the sample view and options to the map view.
private func addViewAnnotation(at coordinate: CLLocationCoordinate2D) {
    let options = ViewAnnotationOptions(
        geometry: Point(coordinate),
        width: 100,
        height: 40,
        allowOverlap: false,
        anchor: .center
    )
    let sampleView = createSampleView(withText: "Hello world!")
    try? mapView.viewAnnotations.add(sampleView, options: options)
}

Add the view annotation on view load:

override func viewDidLoad() {
    super.viewDidLoad()
    // Define a coordinate
    let centerCoordinate = CLLocationCoordinate2D(latitude: 39.7128, longitude: -75.0060)
    // Add a map view
    let options = MapInitOptions(cameraOptions: CameraOptions(center: centerCoordinate, zoom: 7))
    mapView = MapView(frame: view.bounds, mapInitOptions: options)
    mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    view.addSubview(mapView)
    // Add the view annotation
    self.addViewAnnotation(at: centerCoordinate)
}
More view annotation options

There are many more options available when adding or updating the view annotation. See the complete list of options in theViewAnnotationOptions documentation.

Customize the appearance

Specify order

The z-index of view annotations is based on the order in which they are added. This allows you to control which view is visible when multiple views intersect on the screen.

To bring a view annotation on top of others regardless of the order in which it was added, use the selected property:

ViewAnnotationOptions(
  geometry: Point(coordinate),
  allowOverlap: true,
  anchor: .center,
  selected: true
)

Any number of view annotations could be in selected state. The z-index among selected view annotations also respects the order in which they are added.

Default order
View annotations with z-index based on the order in which they were added: Hello world!, Прывітанне Сусвет!, Hallo Welt!, Hei maailma!
Override order
Use the selected property to bring the Hello world! annotation on top of others regardless of the order in which they were added.

View annotations do not rotate, pitch, or scale with the map, but some properties (including width, height, and additional offset in pixels) can be controlled by the user using update operation.

ViewAnnotationOptions(
    geometry: Point(coordinate),
    anchor: .center,
    // Move view annotation to the right on 10 pixels
    offsetX: 10,
    // Move view annotation to the bottom on 20 pixels
    offsetY: -20
)

Handle visibility

When a view annotation is added or updated, visibility can be specified explicitly.

ViewAnnotationOptions(
    geometry: Point(coordinate),
    // Set view annotation to be not visible on the map
    visible: false
)
Do not modify the UIView's isHidden property

Do not modify the UIView's isHidden property directly. When setting visible to false, it will set the view to hidden and also remove it from the layout calculation.

Common uses

The flexibility of view annotations means that there are countless ways to use this feature, but there are a few common uses outlined below to help you get started.

Display on map click

You can add view annotations when a user interacts with the map.

When the user clicks on the map, you can get information from the map camera, position (like in the example linked below), or data in the map to determine the contents of the view annotation using one of MapboxMap's map feature query methods.

example
Add view annotations

Add view annotation on a map click.

Attach to a style layer feature

View annotations are not explicitly bound to any style sources, but ViewAnnotationOptions.associatedFeatureId can be used to bind a given view annotation with a Feature by Feature.id.

The visibility of the feature will then determine the visibility of the view annotation. For example, if the icon is not visible because it will collide with some other icon, the associated view annotation will also disappear from the screen.

override func viewDidLoad() {
    super.viewDidLoad()
    
    // Define a coordinate
    let centerCoordinate = CLLocationCoordinate2D(latitude: 39.7128, longitude: -75.0060)
    let point = Point(centerCoordinate)
    
    // Add a map view
    let options = MapInitOptions(cameraOptions: CameraOptions(center: centerCoordinate, zoom: 7))
    mapView = MapView(frame: view.bounds, mapInitOptions: options)
    mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    view.addSubview(mapView)
    
    // Add the marker and view annotation when the map loads
    mapView.mapboxMap.onNext(.mapLoaded) { [weak self] _ in
        guard let self = self else { return }
        let markerId = self.addMarker(at: point)
        self.addViewAnnotation(at: centerCoordinate, withMarkerId: markerId)
    }
}

// Add a marker image via a symbol layer
private func addMarker(at point: Point) -> String {
    let featureId = "some-feature-id"
    let style = mapView.mapboxMap.style
    
    // Add an image of a red marker to the style
    try? style.addImage(UIImage(named: "red_pin")!, id: "red-pin-image", stretchX: [], stretchY: [])
    
    // Create a GeoJSON source
    var source = GeoJSONSource()
    let sourceId = "some-source-id"
    var feature = Feature(geometry: Geometry.point(point))
    feature.identifier = .string(featureId)
    source.data = .featureCollection(FeatureCollection(features: [feature]))
    try? mapView.mapboxMap.style.addSource(source, id: sourceId)

    // Create a symbol layer using the GeoJSON source
    // and image added above
    var layer = SymbolLayer(id: "some-layer-id")
    layer.source = sourceId
    layer.iconImage = .constant(.name("red-pin-image"))
    layer.iconAnchor = .constant(.bottom)
    layer.iconAllowOverlap = .constant(false)
    try? mapView.mapboxMap.style.addLayer(layer)
    
    return featureId
}

// Update options to attach to a feature
private func addViewAnnotation(at coordinate: CLLocationCoordinate2D, withMarkerId markerId: String? = nil) {
    let options = ViewAnnotationOptions(
        geometry: Point(coordinate),
        width: 100,
        height: 40,
        associatedFeatureId: markerId,
        anchor: .bottom,
        offsetY: 40
    )
    let sampleView = createSampleView(withText: "Hello world!")
    try? mapView.viewAnnotations.add(sampleView, options: options)
}

// Use the same sample view defined in the previous section
private func createSampleView(withText text: String) -> UIView {
    let label = UILabel()
    label.text = text
    label.font = .systemFont(ofSize: 14)
    label.numberOfLines = 0
    label.textColor = .black
    label.backgroundColor = .white
    label.textAlignment = .center
    return label
}
Note

While the View Annotation API does not come with built-in logic for handling opening and closing a callout attached to a symbol layer feature when the feature is click, you can see one possible way to implement that functionality in the Add view annotations connected to features.

example
Add view annotations connected to features

Add view annotation anchored to a symbol layer feature.