Transition from free-drive to turn-by-turn navigation
If you use the NavigationMapView
or NavigationView
class for a free-drive navigation experience and NavigationViewController
for turn-by-turn navigation, you can seamlessly transition between these activities.
Since NavigationMapView
and NavigationView
are usually presented as a child view of a certain view controller, you can use a custom transition based on the UIViewControllerTransitioningDelegate
.
Requirements
To seamlessly transition between free-drive and turn-by-turn navigation sessions, your application needs to meet the following conditions:
- When you present a
NavigationViewController
, set itsmodalPresentationStyle
property tofullScreen
and assign an object that conforms to theUIViewControllerTransitioningDelegate
protocol to itstransitioningDelegate
property. - Make sure that presentation of the
NavigationViewController
is animated. When calling theUIViewController.present(_:animated:completion:)
method, pass intrue
as theanimated
parameter. - Implement classes that conform to the
UIViewControllerAnimatedTransitioning
protocol, which is used when presenting and dismissingNavigationViewController
and takes care of reusing theNavigationMapView
instance.
Consider the following example, where NavigationView
is displayed on the surface of the ViewController
instance, with PresentationTransition
and DismissalTransition
acting as custom animators:
import UIKit
import MapboxDirections
import MapboxCoreNavigation
import MapboxNavigation
import CoreLocation
class ViewController: UIViewController {
private let routingProvider = MapboxRoutingProvider()
var passiveLocationManager: PassiveLocationManager?
var navigationView: NavigationView!
func prepareFreeDriveMapView() {
navigationView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(navigationView)
passiveLocationManager = PassiveLocationManager()
NSLayoutConstraint.activate([
navigationView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
navigationView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
navigationView.topAnchor.constraint(equalTo: view.topAnchor),
navigationView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
navigationView.navigationMapView.navigationCamera.follow()
let navigationViewportDataSource = NavigationViewportDataSource(navigationView.navigationMapView.mapView,
viewportDataSourceType: .passive)
navigationView.navigationMapView.navigationCamera.viewportDataSource = navigationViewportDataSource
}
override func viewDidLoad() {
super.viewDidLoad()
navigationView = NavigationView(frame: view.bounds)
prepareFreeDriveMapView()
let navigationRouteOptions = NavigationRouteOptions(coordinates: [
CLLocationCoordinate2D(latitude: 37.77766, longitude: -122.43199),
CLLocationCoordinate2D(latitude: 37.77536, longitude: -122.43494)
])
// Request a route and present `NavigationViewController`.
routingProvider.calculateRoutes(options: navigationRouteOptions) { [weak self] result in
switch result {
case .failure(let error):
print("Error occured: \(error.localizedDescription)")
case .success(let routeResponse):
guard let self = self else { return }
let navigationService = MapboxNavigationService(indexedRouteResponse: routeResponse,
credentials: NavigationSettings.shared.directions.credentials,
simulating: .always)
let navigationOptions = NavigationOptions(
navigationService: navigationService,
navigationMapView: navigationView.navigationMapView
)
let navigationViewController = NavigationViewController(for: routeResponse, navigationOptions: navigationOptions)
navigationViewController.delegate = self
// Make sure to set `transitioningDelegate` to be a current instance of `ViewController`.
navigationViewController.transitioningDelegate = self
// Make sure to present `NavigationViewController` in animated way.
self.present(navigationViewController, animated: true, completion: nil)
}
}
}
}
// Transition that is used for `NavigationViewController` presentation.
class PresentationTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.0
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromViewController = transitionContext.viewController(forKey: .from) as? ViewController,
let toViewController = transitionContext.viewController(forKey: .to) as? NavigationViewController else {
transitionContext.completeTransition(false)
return
}
// Re-use `NavigationMapView` instance in `NavigationViewController`.
let navigationMapView = fromViewController.navigationView.navigationMapView
toViewController.navigationMapView = navigationMapView
// Clean up free drive location source
fromViewController.passiveLocationManager = nil
// Set active guidance location source
let navigationViewportDataSource = NavigationViewportDataSource(navigationMapView.mapView,
viewportDataSourceType: .active)
navigationMapView.navigationCamera.viewportDataSource = navigationViewportDataSource
transitionContext.containerView.addSubview(toViewController.view)
transitionContext.completeTransition(true)
}
}
// Transition that is used for `NavigationViewController` dismissal.
class DismissalTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.0
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromViewController = transitionContext.viewController(forKey: .from) as? NavigationViewController,
let navigationMapView = fromViewController.navigationMapView,
let toViewController = transitionContext.viewController(forKey: .to) as? ViewController else {
transitionContext.completeTransition(false)
return
}
// Inject `NavigationMapView` instance that was previously used by `NavigationViewController` back to
// `ViewController`.
toViewController.navigationView.navigationMapView = navigationMapView
toViewController.prepareFreeDriveMapView()
// Set up free drive location
toViewController.passiveLocationManager = PassiveLocationManager()
navigationMapView.navigationCamera.viewportDataSource =
NavigationViewportDataSource(navigationMapView.mapView, viewportDataSourceType: .passive)
transitionContext.completeTransition(true)
}
}
extension ViewController: UIViewControllerTransitioningDelegate {
public func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return PresentationTransition()
}
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return DismissalTransition()
}
}
extension ViewController: NavigationViewControllerDelegate {
func navigationViewControllerDidDismiss(
_ navigationViewController: NavigationViewController,
byCanceling canceled: Bool
) {
navigationViewController.dismiss(animated: true)
}
}
Injecting NavigationMapView
When transitioning between free-drive and a turn-by-turn navigation, the NavigationViewController
and another view controller can share an instance of NavigationMapView
. Since the NavigationMapView
configuration can be completely different in these two activities, make sure to do the required changes to this configuration when completing an activity change.
The navigation camera viewport’s data source type
By default, whenever NavigationViewController
is presented, it switches the navigation camera to see location updates in RouteController.routeControllerProgressDidChange
notifications, by setting NavigationViewportDataSource
to the ViewportDataSourceType.active
type. After dismissing NavigationViewController
, you need to switch to the desired viewport data source type:
navigationViewController.dismiss(animated: true) { [weak self] in
guard let self = self else { return }
let navigationMapView = self.viewController.navigationMapView
let navigationViewportDataSource = NavigationViewportDataSource(navigationMapView.mapView,
viewportDataSourceType: .passive)
navigationMapView.navigationCamera.viewportDataSource = navigationViewportDataSource
}
User location style
By default, NavigationViewController
sets NavigationMapView.userLocationStyle
to UserLocationStyle.courseView(_:)
when the turn-by-turn navigation session starts. To preserve the user location style when transitioning from free-drive to turn-by-turn navigation:
viewController.present(navigationViewController,
animated: true,
completion: { [weak self] in
guard let self = self else { return }
navigationViewController.navigationMapView?.userLocationStyle = .puck2D()
}
Banners
For a smooth transition between free-drive and turn-by-turn navigation sessions, you can show and hide the top and bottom banners with animation. Use the navigationView
property of NavigationViewController and PreviewViewController, get the container view using the NavigationView.topBannerContainerView
or NavigationView.bottomBannerContainerView
property, and call the container view’s show(animated:duration:animations:completion:)
or hide(animated:duration:animations:completion:)
method.