> For the complete documentation index, see [llms.txt](https://docs.mapbox.com/ios/maps/llms.txt)

# Draggable view annotations

Your browser doesn't support HTML5 video. Open [link to the video](https://docs.mapbox.com/ios/ios/assets/medias/DraggableViewAnnotation-8986e491c69714d9813050813b122f80.mp4).

**SwiftUI**

This example demonstrates how to make a [`MapViewAnnotation`](https://docs.mapbox.com/ios/maps/api/latest/documentation/mapboxmaps/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`](https://docs.mapbox.com/ios/maps/api/latest/documentation/mapboxmaps/polygonannotation/) or [`PolylineAnnotation`](https://docs.mapbox.com/ios/maps/api/latest/documentation/mapboxmaps/polylineannotation/) geometry in real time, while `onDraggingChanged` is used to animate each annotation's view while it's being dragged.

> **Note: 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.

**UIKit**

This example demonstrates how to make a [`ViewAnnotation`](https://docs.mapbox.com/ios/maps/api/latest/documentation/mapboxmaps/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`](https://docs.mapbox.com/ios/maps/api/latest/documentation/mapboxmaps/polygonannotation/) or [`PolylineAnnotation`](https://docs.mapbox.com/ios/maps/api/latest/documentation/mapboxmaps/polylineannotation/) geometry in real time, while `onDraggingChanged` is used to animate each annotation's view while it's being dragged.

> **Note: Experimental API**
> 
> 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`](https://github.com/mapbox/mapbox-maps-ios/blob/main/Sources/Examples/Models/DraggableViewAnnotationModel.swift) is a small shared model that stores sample coordinates, reused between the SwiftUI and UIKit versions of this example.

> **Note: iOS Examples App Available**
> 
> This example code is part of the **Maps SDK for iOS Examples App**, a working iOS project [available on Github](https://github.com/mapbox/mapbox-maps-ios/tree/11.33.0/Sources/Examples). 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](https://docs.mapbox.com/help/tutorials/maps-sdk-ios-examples-app/) tutorial for step-by-step instructions.

**SwiftUI**

Title: `DraggableViewAnnotationsExample.swift`

[View on GitHub](https://github.com/mapbox/mapbox-maps-ios/blob/main/Sources/Examples/SwiftUI%20Examples/DraggableViewAnnotationsExample.swift)

```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)
    }
}
```

**UIKit**

Title: `DraggableViewAnnotationExample.swift`

[View on GitHub](https://github.com/mapbox/mapbox-maps-ios/blob/main/Sources/Examples/All%20Examples/Annotations/DraggableViewAnnotationExample.swift)

```swift
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)
}
```