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.
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.
This example demonstrates how to make a ViewAnnotation draggable in the Mapbox Maps SDK for iOS by setting isDraggable 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, onDragCoordinateChanged 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.
The isDraggable, isDragging, onDraggingChanged, and onDragCoordinateChanged APIs used in this example are experimental and require @_spi(Experimental) import MapboxMaps. They 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.
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.
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)
}
}
import UIKit
@_spi(Experimental) import MapboxMaps
// MARK: - Draggable View Annotations Example
/// Shows how to make view annotations draggable: set `ViewAnnotation.isDraggable`,
/// observe `onDragCoordinateChanged`/`onDraggingChanged`, and use the updated
/// coordinates to edit map geometry (a polygon or polyline) as the user drags.
///
/// UIKit analog for the SwiftUI ``DraggableViewAnnotationsExample``.
final class ViewController: UIViewController {
private var mapView: MapView!
private var cancelables = Set<AnyCancelable>()
// Polygon
private var polygonManager: PolygonAnnotationManager?
private var polygonVertices = DraggableViewAnnotationData.polygon
// Polyline
private var polylineManager: PolylineAnnotationManager?
private var polylineVertices = DraggableViewAnnotationData.polyline
override func viewDidLoad() {
super.viewDidLoad()
let options = MapInitOptions(cameraOptions: DraggableViewAnnotationData.cameraOptions, styleURI: .outdoors)
mapView = MapView(frame: view.bounds, mapInitOptions: options)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(mapView)
mapView.mapboxMap.onMapLoaded.observeNext { [weak self] _ in
guard let self else { return }
self.addViewAnnotation()
self.addEditablePolygon()
self.addEditablePolyline()
}.store(in: &cancelables)
}
// MARK: View Annotation
private func addViewAnnotation() {
let pinView = CustomPinView()
let annotation = ViewAnnotation(coordinate: DraggableViewAnnotationData.point, view: pinView)
annotation.isDraggable = true
annotation.onDraggingChanged = { [weak pinView] isDragging in
pinView?.setDragging(isDragging)
}
annotation.allowOverlap = true
annotation.variableAnchors = [ViewAnnotationAnchorConfig(anchor: .bottom)]
mapView.viewAnnotations.add(annotation)
}
// MARK: Edit Polygon
private func addEditablePolygon() {
polygonManager = mapView.annotations.makePolygonAnnotationManager()
updatePolygon()
for (index, vertex) in polygonVertices.enumerated() {
let circleView = CustomCircleView(color: .systemGreen)
let annotation = ViewAnnotation(coordinate: vertex.coordinate, view: circleView)
annotation.isDraggable = true
annotation.onDraggingChanged = { [weak circleView] isDragging in
circleView?.setDragging(isDragging)
}
annotation.onDragCoordinateChanged = { [weak self] newCoordinate in
self?.polygonVertices[index].coordinate = newCoordinate
self?.updatePolygon()
}
annotation.allowOverlap = true
mapView.viewAnnotations.add(annotation)
}
}
private func updatePolygon() {
guard let polygonManager else { return }
var ring = polygonVertices.map(\.coordinate)
if let first = ring.first {
ring.append(first)
}
var annotation = PolygonAnnotation(polygon: Polygon([ring]))
annotation.fillColor = StyleColor(.systemGreen)
annotation.fillOpacity = 0.4
polygonManager.annotations = [annotation]
}
// MARK: Edit Polyline
private func addEditablePolyline() {
polylineManager = mapView.annotations.makePolylineAnnotationManager()
updatePolyline()
for (index, vertex) in polylineVertices.enumerated() {
let circleView = CustomCircleView(color: .systemIndigo)
let annotation = ViewAnnotation(coordinate: vertex.coordinate, view: circleView)
annotation.isDraggable = true
annotation.onDraggingChanged = { [weak circleView] isDragging in
circleView?.setDragging(isDragging)
}
annotation.onDragCoordinateChanged = { [weak self] newCoordinate in
self?.polylineVertices[index].coordinate = newCoordinate
self?.updatePolyline()
}
annotation.allowOverlap = true
mapView.viewAnnotations.add(annotation)
}
}
private func updatePolyline() {
guard let polylineManager else { return }
var annotation = PolylineAnnotation(lineCoordinates: polylineVertices.map(\.coordinate))
annotation.lineColor = StyleColor(.systemIndigo)
annotation.lineWidth = 4
polylineManager.annotations = [annotation]
}
}
// MARK: - Helper Views
private final class CustomPinView: UIView {
private let imageView = UIImageView(image: UIImage(named: "dest-pin"))
init() {
super.init(frame: CGRect(x: 0, y: 0, width: 40, height: 52))
imageView.frame = bounds
imageView.contentMode = .scaleAspectFit
addSubview(imageView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
CGSize(width: 40, height: 52)
}
func setDragging(_ isDragging: Bool) {
animateSpring {
self.imageView.transform = isDragging ? CGAffineTransform(scaleX: 1.2, y: 1.2) : .identity
}
}
}
private final class CustomCircleView: UIView {
private let circleView: UIView
init(color: UIColor) {
circleView = UIView(frame: CGRect(x: 11, y: 11, width: 22, height: 22))
super.init(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
circleView.backgroundColor = .white
circleView.layer.cornerRadius = 11
circleView.layer.borderWidth = 2
circleView.layer.borderColor = color.cgColor
addSubview(circleView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
CGSize(width: 44, height: 44)
}
func setDragging(_ isDragging: Bool) {
let size: CGFloat = isDragging ? 30 : 22
animateSpring {
self.circleView.bounds.size = CGSize(width: size, height: size)
self.circleView.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
self.circleView.layer.cornerRadius = size / 2
}
}
}
// UIKit analog of SwiftUI's `.animation(.interactiveSpring(), value: isDragging)`.
private func animateSpring(_ animations: @escaping () -> Void) {
UIView.animate(
withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.86, initialSpringVelocity: 0,
options: [.allowUserInteraction], animations: animations)
}