Skip to main content

Frame a list of view annotations

Animates to a camera which frames the list of selected view annotations.
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.

FrameViewAnnotationsExample.swift
import UIKit
import MapboxMaps

final class ViewController: UIViewController {
private enum Animator {
case flyTo, easeTo, viewport
}

private var cancelables = Set<AnyCancelable>()
private var flyToButton: UIButton!
private var easeToButton: UIButton!
private var viewportButton: UIButton!
private var resetButton: UIButton!
private var annotations = [ViewAnnotation]()

private var mapView: MapView!
private let initialCamera = CameraOptions(
center: .random,
padding: UIEdgeInsets(top: .random(in: 0...20), left: .random(in: 0...20), bottom: .random(in: 0...20), right: .random(in: 0...20)),
zoom: 0,
bearing: 0,
pitch: 0
)

override func viewDidLoad() {
super.viewDidLoad()

view.backgroundColor = .white

mapView = MapView(frame: view.bounds, mapInitOptions: MapInitOptions(cameraOptions: initialCamera))
// Camera
try! mapView.mapboxMap.setProjection(StyleProjection(name: .mercator))
let buttonsView = makeButtonsView()

view.addSubview(mapView)
view.addSubview(buttonsView)

mapView.translatesAutoresizingMaskIntoConstraints = false
buttonsView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
mapView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
buttonsView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
buttonsView.leadingAnchor.constraint(greaterThanOrEqualTo: view.safeAreaLayoutGuide.leadingAnchor),
buttonsView.bottomAnchor.constraint(equalTo: mapView.ornaments.logoView.topAnchor, constant: -10),
])

addAnnotations()

mapView.mapboxMap.onMapLoaded.observeNext { [weak self] _ in


}.store(in: &cancelables)
}

private func makeButtonsView() -> UIView {
func makeButton(title: String, selector: Selector) -> UIButton {
let button = UIButton()
button.setTitle(title, for: .normal)
button.contentEdgeInsets = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
button.backgroundColor = .black
button.addTarget(self, action: selector, for: .touchUpInside)
return button
}

flyToButton = makeButton(title: "FlyTo", selector: #selector(flyToButtonTapped(_:)))
easeToButton = makeButton(title: "EaseTo", selector: #selector(easeToButtonTapped(_:)))
viewportButton = makeButton(title: "Viewport", selector: #selector(viewportButtonTapped(_:)))
resetButton = makeButton(title: "Reset camera", selector: #selector(resetButtonTapped(_:)))

let buttonsView = UIStackView(arrangedSubviews: [flyToButton, easeToButton, viewportButton, resetButton])
buttonsView.axis = .horizontal
buttonsView.spacing = 10
buttonsView.distribution = .fillEqually

resetButton.isHidden = true

return buttonsView
}

@objc private func flyToButtonTapped(_ sender: UIButton) {
frameViewAnnotation(with: .flyTo, sender: sender)
}

@objc private func easeToButtonTapped(_ sender: UIButton) {
frameViewAnnotation(with: .easeTo, sender: sender)
}

@objc private func viewportButtonTapped(_ sender: UIButton) {
frameViewAnnotation(with: .viewport, sender: sender)
}

@objc private func resetButtonTapped(_ sender: UIButton) {
mapView.viewport.idle()
mapView.mapboxMap.setCamera(to: initialCamera)
resetButton.isHidden = true
flyToButton.isHidden = false
easeToButton.isHidden = false
viewportButton.isHidden = false
}

private func frameViewAnnotation(with animator: Animator, sender: UIButton) {
flyToButton.isHidden = true
easeToButton.isHidden = true
viewportButton.isHidden = true
resetButton.isHidden = false

let camera = self.mapView.viewAnnotations.camera(
forAnnotations: annotations,
padding: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10),
bearing: nil,
pitch: nil
)!

switch animator {
case .flyTo:
mapView.camera.fly(to: camera, duration: 1)
case .easeTo:
mapView.camera.ease(to: camera, duration: 1)
case .viewport:
let bounds = mapView.mapboxMap.coordinateBounds(for: camera)
let overviewViewportStateOptions = OverviewViewportStateOptions(
geometry: MultiPoint([bounds.northeast, bounds.southeast, bounds.southwest, bounds.northwest]),
bearing: camera.bearing,
pitch: camera.pitch,
animationDuration: 1
)
let overviewViewportState = mapView.viewport.makeOverviewViewportState(options: overviewViewportStateOptions)
mapView.viewport.transition(to: overviewViewportState)
}
}

private func addAnnotations() {
func makeView(text: String) -> UIView {
let view = UIView()
view.backgroundColor = .white
view.layer.shadowOpacity = 0.25
view.layer.shadowRadius = 8
view.layer.shadowOffset = CGSize(width: 0, height: 2)
view.layer.cornerRadius = 8
let label = UILabel()
label.text = text
label.textColor = .black
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 4),
label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -4),
label.topAnchor.constraint(equalTo: view.topAnchor, constant: 4),
label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -4),
])
return view
}

self.annotations = annotationData.map {
let view = makeView(text: $0.name)
let annotation = ViewAnnotation(coordinate: $0.coordinate, view: view)
annotation.variableAnchors = [.init(anchor: $0.anchor)]
self.mapView.viewAnnotations.add(annotation)
return annotation
}
}

private struct AnnotationInfo {
var name: String
var coordinate: CLLocationCoordinate2D
var anchor: ViewAnnotationAnchor
}

private let annotationData: [AnnotationInfo] = [
AnnotationInfo(name: "Saigon", coordinate: .init(latitude: 10.823099, longitude: 106.629662),
anchor: .top),
AnnotationInfo(name: "Hanoi", coordinate: .init(latitude: 21.027763, longitude: 105.834160),
anchor: .bottomLeft),
AnnotationInfo(name: "Tokyo", coordinate: .init(latitude: 35.689487, longitude: 139.691711),
anchor: .right),
AnnotationInfo(name: "Bangkok", coordinate: .init(latitude: 13.756331, longitude: 100.501762),
anchor: .topRight),
AnnotationInfo(name: "Jakarta", coordinate: .init(latitude: -6.175110, longitude: 106.865036),
anchor: .topLeft)
]
}

private func annotation(geometry: GeometryConvertible, width: CGFloat, height: CGFloat, anchor: ViewAnnotationAnchor) -> ViewAnnotationOptions {
.init(
annotatedFeature: .geometry(geometry),
width: width,
height: height,
allowOverlap: true,
variableAnchors: [ViewAnnotationAnchorConfig(anchor: anchor)])
}
Was this example helpful?