Skip to main content
For the complete documentation index, see llms.txt.

Draggable view annotations

This example demonstrates how to make a MapViewAnnotation draggable in the Mapbox Maps SDK for iOS by binding its coordinate to a Binding<CLLocationCoordinate2D> on three view annotations: one showing a pin icon, and two shown as circles marking a vertex of a polygon and a polyline. As the user drags a view, the updated binding is used to update the underlying PolygonAnnotation or PolylineAnnotation geometry in real time, while onDraggingChanged is used to animate each annotation's view while it's being dragged.

Experimental API

The MapViewAnnotation(coordinate: Binding<CLLocationCoordinate2D>, content:, onDraggingChanged:) initializer used in this example is experimental and requires @_spi(Experimental) import MapboxMaps. It may change in future releases.

DraggableViewAnnotationData is a small shared model that stores sample coordinates, reused between the SwiftUI and UIKit versions of this example.

iOS Examples App Available

This example code is part of the Maps SDK for iOS Examples App, a working iOS project available on Github. iOS developers are encouraged to run the examples app locally to interact with this example in an emulator and explore other features of the Maps SDK.

See our Run the Maps SDK for iOS Examples App tutorial for step-by-step instructions.

DraggableViewAnnotationsExample.swift
import SwiftUI
@_spi(Experimental) import MapboxMaps

// MARK: - Draggable View Annotations Example

/// Shows how to make view annotations draggable: bind a coordinate via
/// `MapViewAnnotation(coordinate:content:onDraggingChanged:)` and use the updated
/// coordinates to edit map geometry (a polygon or polyline) as the user drags.
///
/// SwiftUI analog for the UIKit ``DraggableViewAnnotationExample``.
struct DraggableViewAnnotationsExample: View {
// View
@State private var viewAnnotationCoordinate = DraggableViewAnnotationData.point
@State private var isViewAnnotationDragging = false

// Polygon
@State private var polygonVertices = DraggableViewAnnotationData.polygon
@State private var draggedPolygonVertexId: String?

// Polyline
@State private var polylineVertices = DraggableViewAnnotationData.polyline
@State private var draggedPolylineVertexId: String?

var body: some View {
Map(initialViewport: .camera(DraggableViewAnnotationData.cameraOptions)) {
viewAnnotationContent
editPolygonContent
editPolylineContent
}
.mapStyle(.outdoors)
.ignoresSafeArea()
}

// MARK: View Annotation

@MapContentBuilder
private var viewAnnotationContent: some MapContent {
MapViewAnnotation(coordinate: $viewAnnotationCoordinate) {
CustomPinView(isDragging: isViewAnnotationDragging)
} onDraggingChanged: { dragging in
isViewAnnotationDragging = dragging
}
.allowOverlap(true)
.variableAnchors([ViewAnnotationAnchorConfig(anchor: .bottom)])
}

// MARK: Edit Polygon

@MapContentBuilder
private var editPolygonContent: some MapContent {
PolygonAnnotation(polygon: Polygon([closedRing(polygonVertices.map(\.coordinate))]))
.fillColor(StyleColor(.systemGreen))
.fillOpacity(0.4)

ForEvery(polygonVertices) { vertex in
MapViewAnnotation(coordinate: coordinateBinding(in: $polygonVertices, for: vertex.id)) {
CustomCircleView(isDragging: draggedPolygonVertexId == vertex.id, color: Color(uiColor: .systemGreen))
} onDraggingChanged: { dragging in
draggedPolygonVertexId = dragging ? vertex.id : nil
}
.allowOverlap(true)
}
}

// MARK: Edit Polyline

@MapContentBuilder
private var editPolylineContent: some MapContent {
PolylineAnnotation(lineCoordinates: polylineVertices.map(\.coordinate))
.lineColor(StyleColor(.systemIndigo))
.lineWidth(4)

ForEvery(polylineVertices) { vertex in
MapViewAnnotation(coordinate: coordinateBinding(in: $polylineVertices, for: vertex.id)) {
CustomCircleView(isDragging: draggedPolylineVertexId == vertex.id, color: Color(uiColor: .systemIndigo))
} onDraggingChanged: { dragging in
draggedPolylineVertexId = dragging ? vertex.id : nil
}
.allowOverlap(true)
}
}

// MARK: Helpers

private func coordinateBinding(
in vertices: Binding<[DraggableViewAnnotationData.Vertex]>,
for id: String
) -> Binding<CLLocationCoordinate2D> {
Binding(
get: { vertices.wrappedValue.first(where: { $0.id == id })?.coordinate ?? CLLocationCoordinate2D() },
set: { newValue in
if let index = vertices.wrappedValue.firstIndex(where: { $0.id == id }) {
vertices.wrappedValue[index].coordinate = newValue
}
}
)
}

private func closedRing(_ coordinates: [CLLocationCoordinate2D]) -> [CLLocationCoordinate2D] {
guard let first = coordinates.first else { return coordinates }
return coordinates + [first]
}
}

private extension Viewport {
static func camera(_ options: CameraOptions) -> Viewport {
.camera(
center: options.center,
anchor: options.anchor,
zoom: options.zoom,
bearing: options.bearing,
pitch: options.pitch
)
}
}

// MARK: - Helper Views

private struct CustomPinView: View {
let isDragging: Bool

var body: some View {
Image(uiImage: UIImage(named: "dest-pin")!)
.scaleEffect(isDragging ? 1.2 : 1.0)
.animation(.interactiveSpring(), value: isDragging)
}
}

private struct CustomCircleView: View {
let isDragging: Bool
let color: Color

var body: some View {
Circle()
.fill(.white)
.overlay(Circle().stroke(color, lineWidth: 2))
.frame(
width: isDragging ? 30 : 22,
height: isDragging ? 30 : 22
)
.animation(.interactiveSpring(), value: isDragging)
.frame(width: 44, height: 44)
}
}
Was this example helpful?