Skip to main content

Add view annotations connected to features

Add a ViewAnnotation to a MapView and anchor it to a feature in a symbol layer.
Add AnnotationView class to your project

This example uses AnnotationView, a custom class defined in the Maps SDK for the iOS Examples App. To use this code snippet, you must also add the AnnotationView class to your project.

ViewAnnotationMarkerExample.swift
import UIKit
import MapboxMaps
import CoreLocation

final class ViewController: UIViewController {
private var mapView: MapView!
private var pointList: [Feature] = []
private var markerId = 0
private var annotations = [String: ViewAnnotation]()

private let image = UIImage(named: "intermediate-pin")!
private lazy var markerHeight: CGFloat = image.size.height
private var cancelables = Set<AnyCancelable>()

lazy var styleChangeButton: UIButton = {
let button = UIButton(type: .system)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = .systemTeal
button.layer.cornerRadius = 8
button.clipsToBounds = true
button.setTitle("Change style", for: .normal)
button.addTarget(self, action: #selector(styleChangePressed(sender:)), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()

override func viewDidLoad() {
super.viewDidLoad()

let centerCoordinate = CLLocationCoordinate2D(latitude: 39.7128, longitude: -75.0060)
let options = MapInitOptions(cameraOptions: CameraOptions(center: centerCoordinate, zoom: 7))

mapView = MapView(frame: view.bounds, mapInitOptions: options)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(mapView)

mapView.mapboxMap.onMapLoaded.observeNext { [weak self] _ in
guard let self = self else { return }

}.store(in: &cancelables)

mapView.mapboxMap.onStyleLoaded.observe { [weak self, weak mapView] _ in
guard let self, let mapView else { return }
self.prepareStyle()
self.addMarker(at: mapView.mapboxMap.coordinate(for: mapView.center), viewAnnotation: true)
}.store(in: &cancelables)

mapView.gestures.onMapLongPress.observe { [weak self] context in
self?.addMarker(at: context.coordinate)
}.store(in: &cancelables)

mapView.gestures.onLayerTap(Constants.LAYER_ID) { [weak self] feature, _ in
self?.handleMarkerTap(feature.feature) ?? false
}.store(in: &cancelables)

mapView.mapboxMap.styleURI = .streets

view.addSubview(styleChangeButton)

NSLayoutConstraint.activate([
styleChangeButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -32),
styleChangeButton.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: -16),
styleChangeButton.widthAnchor.constraint(equalToConstant: 128)
])
}

private func handleMarkerTap(_ feature: Feature) -> Bool {
guard case let .string(id) = feature.identifier else { return false }

if let annotation = annotations[id] {
annotation.visible.toggle()
return true
}
return addViewAnnotation(to: feature)
}

@objc private func styleChangePressed(sender: UIButton) {
mapView.mapboxMap.styleURI = mapView.mapboxMap.styleURI == .streets ? .satelliteStreets : .streets
}

// MARK: - Style management

private func prepareStyle() {
try? mapView.mapboxMap.addImage(image, id: Constants.BLUE_ICON_ID)

var source = GeoJSONSource(id: Constants.SOURCE_ID)
source.data = .featureCollection(FeatureCollection(features: pointList))
try? mapView.mapboxMap.addSource(source)

if mapView.mapboxMap.styleURI == .satelliteStreets {
var demSource = RasterDemSource(id: "terrain-source")
demSource.url = Constants.TERRAIN_URL_TILE_RESOURCE
try? mapView.mapboxMap.addSource(demSource)
let terrain = Terrain(sourceId: demSource.id)
try? mapView.mapboxMap.setTerrain(terrain)
}

var layer = SymbolLayer(id: Constants.LAYER_ID, source: Constants.SOURCE_ID)
layer.iconImage = .constant(.name(Constants.BLUE_ICON_ID))
layer.iconAnchor = .constant(.bottom)
layer.iconOffset = .constant([0, 12])
layer.iconAllowOverlap = .constant(true)
try? mapView.mapboxMap.addLayer(layer)
}

// MARK: - Annotation management

// Add a marker to a custom GeoJSON source:
// This is an optional step to demonstrate the automatic alignment of view annotations
// with features in a data source
private func addMarker(at coordinate: CLLocationCoordinate2D, viewAnnotation: Bool = false) {
let currentId = "\(Constants.MARKER_ID_PREFIX)\(markerId)"
markerId += 1
var feature = Feature(geometry: Point(coordinate))
feature.identifier = .string(currentId)
pointList.append(feature)
if (try? mapView.mapboxMap.source(withId: Constants.SOURCE_ID)) != nil {
mapView.mapboxMap.updateGeoJSONSource(withId: Constants.SOURCE_ID, geoJSON: .featureCollection(FeatureCollection(features: pointList)))
}

if viewAnnotation {
addViewAnnotation(to: feature)
}
}

// Add a view annotation at a specified location and optionally bind it to an ID of a marker
@discardableResult
private func addViewAnnotation(to feature: Feature) -> Bool {
guard case let .string(id) = feature.identifier,
case let .point(point) = feature.geometry else { return false }
let annotationView = AnnotationView(frame: .zero)
annotationView.title = String(format: "lat=%.2f\nlon=%.2f", point.coordinates.latitude, point.coordinates.longitude)

let annotation = ViewAnnotation(
annotatedFeature: .layerFeature(layerId: Constants.LAYER_ID, featureId: id),
view: annotationView)
annotation.variableAnchors = [ViewAnnotationAnchorConfig(anchor: .bottom, offsetY: markerHeight - 12)]
mapView.viewAnnotations.add(annotation)

annotationView.onClose = { [weak annotation, weak self] in
annotation?.remove()
self?.annotations.removeValue(forKey: id)
}
annotationView.onSelect = { [weak annotation] selected in
annotation?.selected = selected
annotation?.setNeedsUpdateSize()
}

annotations[id] = annotation
return true
}
}

extension ViewController {
private enum Constants {
static let BLUE_ICON_ID = "blue"
static let SOURCE_ID = "source_id"
static let LAYER_ID = "layer_id"
static let TERRAIN_URL_TILE_RESOURCE = "mapbox://mapbox.mapbox-terrain-dem-v1"
static let MARKER_ID_PREFIX = "view_annotation_"
static let SELECTED_ADD_COEF_PX: CGFloat = 50
}
}
Was this example helpful?