Animate a view annotation
This example demonstrates how to create an animated ViewAnnotation
on a map created with the Mapbox Maps SDK for iOS. The example sets up a MapView
with a predefined route line displayed and animates an annotation along this route.
The annotation animation is achieved by calculating the progress based on a specified duration and updating the annotation's coordinates. The code includes setup for the MapView
, loading a GeoJSON route from a GeoJSONSource
, and defining annotation properties such as the annotation view and anchor points.
This example uses AnnotationView
. To use this code snippet, you must also add the AnnotationView class, a custom class defined in the Maps SDK for the iOS Examples App.
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 UIKit
import MapboxMaps
import CoreLocation
final class ViewController: UIViewController {
private var mapView: MapView!
private var cancelables = Set<AnyCancelable>()
private lazy var route: LineString = {
let routeURL = Bundle.main.url(forResource: "sf_airport_route", withExtension: "geojson")!
let routeData = try! Data(contentsOf: routeURL)
return try! JSONDecoder().decode(LineString.self, from: routeData)
}()
private lazy var totalDistance: CLLocationDistance = route.distance() ?? 0
private var annotation: ViewAnnotation?
private var animationStartTime: TimeInterval = 0
override func viewDidLoad() {
super.viewDidLoad()
// center camera around SF airport
let centerCoordinate = CLLocationCoordinate2D(latitude: 37.7080221537549, longitude: -122.39470445734368)
let options = MapInitOptions(cameraOptions: CameraOptions(center: centerCoordinate, zoom: 11))
mapView = MapView(frame: view.bounds, mapInitOptions: options)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(mapView)
mapView.mapboxMap.onStyleLoaded.observeNext { [weak self] _ in
guard let self = self else { return }
self.setupExample()
}.store(in: &cancelables)
}
private func setupExample() {
var source = GeoJSONSource(id: "route-source")
source.data = .geometry(route.geometry)
try! mapView.mapboxMap.addSource(source)
var layer = LineLayer(id: "route-layer", source: source.id)
layer.lineColor = .constant(StyleColor(UIColor.systemPink))
layer.lineWidth = .constant(4)
try! mapView.mapboxMap.addLayer(layer)
let view = UIImageView(image: UIImage(named: "intermediate-pin"))
view.contentMode = .scaleAspectFit
let annotation = ViewAnnotation(coordinate: route.coordinates.first!, view: view)
annotation.variableAnchors = [.init(anchor: .bottom, offsetY: -12)]
mapView.viewAnnotations.add(annotation)
self.annotation = annotation
}
else {
mapView.mapboxMap.onMapLoaded.observeNext { _ in
self.startAnimation()
}.store(in: &cancelables)
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
animationStartTime = 0 // stops the animation
}
private func startAnimation() {
let link = CADisplayLink(target: self, selector: #selector(animateNextStep))
link.add(to: .main, forMode: .default)
animationStartTime = CACurrentMediaTime()
}
@objc private func animateNextStep(_ displayLink: CADisplayLink) {
let animationDuration: TimeInterval = 30
let progress = (CACurrentMediaTime() - animationStartTime) / animationDuration
let currentDistanceOffset = totalDistance * min(progress, 1)
defer {
if progress >= 1 {
displayLink.invalidate()
}
}
let currentCoordinate = route.coordinateFromStart(distance: currentDistanceOffset)!
// set new coordinate to the annotation
annotation?.annotatedFeature = .geometry(Point(currentCoordinate))
}
}