Advanced viewport gestures
This example configures the viewport
behavior in an app using the Mapbox Maps SDK for iOS. It allows the viewport to switch between a follow
state and an overview
state triggered by a single tap on the map. In the "follow puck" state, zoom and pitch gestures are enabled alongside automatic updates from the viewport state. For a comprehensive user experience, it is recommended to simulate the example with the "Features > Location > Freeway Drive" setting in the iOS simulator.
The implementation involves managing the different states follow
and overview
utilizing FollowPuckViewportState
and OverviewViewportState
to control the viewport behavior. Additionally, the example demonstrates customization of gesture interactions based on the viewport state, such as enabling or disabling pan and pinch gestures. The interactions trigger transitions in the viewport, providing a smooth user experience through seamless state changes. The example also includes gesture management through the GestureManagerDelegate
extension and monitoring viewport status changes via the ViewportStatusObserver
protocol.
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
// This example configures the viewport so that when it's in the follow puck state,
// zoom and pitch gestures work alongside updates coming from the state itself.
// Single tap on the map to switch to the overview state.
//
// When trying this example in the simulator, choose Features > Location > Freeway Drive
// to get a good sense of the resulting user experience.
final class ViewController: UIViewController {
private enum State {
case following
case overview
}
private var state: State = .following {
didSet {
syncWithState()
}
}
private var mapView: MapView!
private var followPuckViewportState: FollowPuckViewportState!
private var overviewViewportState: OverviewViewportState!
override func viewDidLoad() {
super.viewDidLoad()
mapView = MapView(frame: view.bounds)
view.addSubview(mapView)
let cupertino = CLLocationCoordinate2D(latitude: 37.3230, longitude: -122.0322)
mapView.mapboxMap.setCamera(to: CameraOptions(center: cupertino, zoom: 14))
mapView.location.options.puckType = .puck2D(.makeDefault(showBearing: true))
mapView.location.options.puckBearing = .course
mapView.location.options.puckBearingEnabled = true
followPuckViewportState = mapView.viewport.makeFollowPuckViewportState(
options: FollowPuckViewportStateOptions(
bearing: .course))
overviewViewportState = mapView.viewport.makeOverviewViewportState(
options: OverviewViewportStateOptions(
geometry: Polygon(
center: cupertino,
radius: 20000,
vertices: 100)))
mapView.gestures.delegate = self
syncWithState()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
// break strong reference cycle
mapView.viewport.removeStatusObserver(self)
}
private func toggleViewportState() {
switch state {
case .overview:
state = .following
case .following:
state = .overview
}
}
private func syncWithState() {
switch state {
case .following:
mapView.viewport.transition(to: followPuckViewportState)
case .overview:
mapView.viewport.transition(to: overviewViewportState)
}
mapView.viewport.options.transitionsToIdleUponUserInteraction = state == .overview
mapView.gestures.options.panEnabled = state == .overview
mapView.gestures.options.pinchEnabled = state == .overview
}
}
extension ViewController: GestureManagerDelegate {
func gestureManager(_ gestureManager: GestureManager, didBegin gestureType: GestureType) {
switch gestureType {
case .pitch:
if state == .following {
followPuckViewportState.options.pitch = nil
}
case .doubleTapToZoomIn, .doubleTouchToZoomOut, .quickZoom:
if state == .following {
followPuckViewportState.options.zoom = nil
}
default:
break
}
}
func gestureManager(_ gestureManager: GestureManager, didEnd gestureType: GestureType, willAnimate: Bool) {
switch gestureType {
case .pitch:
if state == .following {
followPuckViewportState.options.pitch = mapView.mapboxMap.cameraState.pitch
}
case .quickZoom:
if state == .following {
followPuckViewportState.options.zoom = mapView.mapboxMap.cameraState.zoom
}
case .singleTap:
toggleViewportState()
default:
break
}
}
func gestureManager(_ gestureManager: GestureManager, didEndAnimatingFor gestureType: GestureType) {
switch gestureType {
case .doubleTapToZoomIn, .doubleTouchToZoomOut:
if state == .following {
followPuckViewportState.options.zoom = mapView.mapboxMap.cameraState.zoom
}
default:
break
}
}
}
extension ViewController: ViewportStatusObserver {
func viewportStatusDidChange(
from fromStatus: ViewportStatus,
to toStatus: ViewportStatus,
reason: ViewportStatusChangeReason) {
print("Viewport.status changed\n from: \(fromStatus)\n to: \(toStatus)\n with reason: \(reason)")
}
}