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 itsmodalPresentationStyleproperty tofullScreenand assign an object that conforms to theUIViewControllerTransitioningDelegateprotocol to itstransitioningDelegateproperty. - Make sure that presentation of the
NavigationViewControlleris animated. When calling theUIViewController.present(_:animated:completion:)method, pass intrueas theanimatedparameter. - Implement classes that conform to the
UIViewControllerAnimatedTransitioningprotocol, which is used when presenting and dismissingNavigationViewControllerand takes care of reusing theNavigationMapViewinstance.
Consider the following example, where NavigationView is displayed on the surface of the ViewController instance, with PresentationTransition and DismissalTransition acting as custom animators:
class ViewController: UIViewController {
var navigationView: NavigationView!
let navigationProvider = MapboxNavigationProvider(coreConfig: CoreConfig())
override func viewDidLoad() {
super.viewDidLoad()
navigationProvider.tripSession().startFreeDrive()
navigationView = NavigationView(
frame: view.bounds,
mapViewConfiguration: .createNew(
location: navigationProvider.navigation().locationMatching
.map(\.mapMatchingResult.enhancedLocation)
.eraseToAnyPublisher(),
routeProgress: navigationProvider.navigation().routeProgress
.map(\.?.routeProgress)
.eraseToAnyPublisher(),
predictiveCacheManager: navigationProvider.predictiveCacheManager
)
)
navigationView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(navigationView)
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)
])
Task { [weak self] in
guard let self = self else { return }
do {
let navigationRouteOptions = NavigationRouteOptions(coordinates: [
CLLocationCoordinate2D(latitude: 37.77766, longitude: -122.43199),
CLLocationCoordinate2D(latitude: 37.77536, longitude: -122.43494)
])
// Request routes and present `NavigationViewController`.
let navigationRoutes = try await navigationProvider.routingProvider().calculateRoutes(options: navigationRouteOptions).value
let navigationViewController = NavigationViewController(
navigationRoutes: navigationRoutes,
navigationOptions: NavigationOptions(
mapboxNavigation: navigationProvider.mapboxNavigation,
voiceController: navigationProvider.routeVoiceController,
eventsManager: navigationProvider.eventsManager(),
styles: [NightStyle()],
predictiveCacheManager: navigationProvider.predictiveCacheManager
)
)
// Make sure to set `transitioningDelegate` to be a current instance of `ViewController`.
navigationViewController.transitioningDelegate = self
navigationViewController.modalPresentationStyle = .fullScreen
// Make sure to present `NavigationViewController` in animated way.
self.present(navigationViewController, animated: true, completion: nil)
} catch {
print("Error occured while requesting routes: \(error.localizedDescription)")
}
}
}
}
// 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`.
toViewController.navigationMapView = fromViewController.navigationView.navigationMapView
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
transitionContext.containerView.addSubview(toViewController.view)
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()
}
}
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 make the required changes to this configuration when completing an activity change.
NavigationMapView delegate
NavigationViewController changes the NavigationMapView delegate. If you use your own NavigationMapViewDelegate implementation, make sure to reset it back when NavigationViewController is no longer active.
navigationViewController.dismiss(animated: true) { [weak self] in
guard let self else { return }
navigationView.navigationMapView.delegate = self
}
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.