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.
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:
- Defining options for the view annotation.
- Creating the sample view from above.
- 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)
}
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.
Control behavior related to camera
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 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.
Add view annotation on a map click.
Attach to a point annotation
You can tie a view annotation's visibility to a point annotation's visibility using ViewAnnotationOptions.associatedFeatureId
. This allows implementing marker pop-ups with a minimal amount of code and makes the view annotation disappear automatically if the point annotation image is not visible due to overlap or collision.
// prepare point annotation
var pointAnnotation = PointAnnotation(coordinate: coordinate)
pointAnnotation.iconImage = "marker-image-name"
pointAnnotation.iconAnchor = .bottom
let pointAnnotationManager = mapView.annotations.makePointAnnotationManager()
pointAnnotationManager.annotations = [pointAnnotation]
// prepare view annotation and connect it by feature id
let options = ViewAnnotationOptions(
geometry: Point(coordinate),
associatedFeatureId: pointAnnotation.id,
anchor: .bottom,
offsetY: markerImageHeight
)
let annotationView = AnnotationView(frame: CGRect(x: 0, y: 0, width: 128, height: 64))
try? mapView.viewAnnotations.add(annotationView, options: options)
Add view annotation anchored to a point annotation.
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
}
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.
Add view annotation anchored to a symbol layer feature.