Skip to content

Commit ebdf000

Browse files
authored
Add the route preview and cancel function in CarPlay (#4311)
1 parent cc0889f commit ebdf000

File tree

9 files changed

+131
-36
lines changed

9 files changed

+131
-36
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## v2.11.0
44

5+
### CarPlay
6+
7+
* Added `CarPlayManagerDelegate.carPlayManagerDidCancelPreview(_:)` to notify developers after CarPlay canceled routes preview, and `CarPlayManager.cancelRoutesPreview()` method to cancel routes preview on CarPlay. ([#4311](https://github.com/mapbox/mapbox-navigation-ios/pull/4311))
8+
* Added `CPRouteChoice.indexedRouteResponse` property to allow developers to get access to the `IndexedRouteResponse` of `CPRouteChoice` on CarPlay. ([#4311](https://github.com/mapbox/mapbox-navigation-ios/pull/4311))
9+
510
### Other changes
611

712
* Fixed an issue where an incorrect upcoming intersection index cause a crash. ([#4314](https://github.com/mapbox/mapbox-navigation-ios/pull/4314))

Example/AppDelegate+CarPlay.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,23 @@ extension AppDelegate: CPApplicationDelegate {
6767
// MARK: - CarPlayManagerDelegate methods
6868

6969
extension AppDelegate: CarPlayManagerDelegate {
70+
func carPlayManager(_ carPlayManager: CarPlayManager, selectedPreviewFor trip: CPTrip, using routeChoice: CPRouteChoice) {
71+
guard let indexedRouteResponse = routeChoice.indexedRouteResponse,
72+
shouldPreviewRoutes(for: indexedRouteResponse) else { return }
73+
currentAppRootViewController?.indexedRouteResponse = indexedRouteResponse
74+
}
75+
76+
private func shouldPreviewRoutes(for indexedRouteResponse: IndexedRouteResponse) -> Bool {
77+
guard let rootResponse = currentAppRootViewController?.indexedRouteResponse else {
78+
return true
79+
}
80+
return indexedRouteResponse.routeResponse.routes != rootResponse.routeResponse.routes ||
81+
indexedRouteResponse.routeIndex != rootResponse.routeIndex
82+
}
83+
84+
func carPlayManagerDidCancelPreview(_ carPlayManager: CarPlayManager) {
85+
currentAppRootViewController?.indexedRouteResponse = nil
86+
}
7087

7188
func carPlayManager(_ carPlayManager: CarPlayManager,
7289
navigationServiceFor indexedRouteResponse: IndexedRouteResponse,
@@ -484,6 +501,8 @@ class CarPlaySceneDelegate: NSObject, CPTemplateApplicationSceneDelegate {
484501
// navigation as well, otherwise, CarPlay will be in passive navigation and stay out of sync with iOS app.
485502
if appDelegate.currentAppRootViewController?.activeNavigationViewController != nil {
486503
appDelegate.currentAppRootViewController?.beginCarPlayNavigation()
504+
} else if let indexedRouteResponse = appDelegate.currentAppRootViewController?.indexedRouteResponse {
505+
appDelegate.carPlayManager.previewRoutes(for: indexedRouteResponse)
487506
}
488507
}
489508

Example/ViewController.swift

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ class ViewController: UIViewController {
7575
navigationMapView.showcase(routeResponse)
7676
navigationMapView.showWaypoints(on: prioritizedRoutes.first!)
7777
navigationMapView.showRouteDurations(along: prioritizedRoutes)
78+
79+
startButton.isEnabled = true
80+
clearMap.isHidden = false
81+
82+
updateCarPlayRoutesPreview()
7883
}
7984

8085
var currentRoute: Route? {
@@ -93,8 +98,6 @@ class ViewController: UIViewController {
9398
clearNavigationMapView()
9499
return
95100
}
96-
97-
startButton.isEnabled = true
98101
showCurrentRoute()
99102
}
100103
}
@@ -226,6 +229,17 @@ class ViewController: UIViewController {
226229
navigationMapView?.removeContinuousAlternativesRoutes()
227230

228231
waypoints.removeAll()
232+
navigationMapView?.navigationCamera.follow()
233+
updateCarPlayRoutesPreview()
234+
}
235+
236+
private func updateCarPlayRoutesPreview() {
237+
guard let delegate = UIApplication.shared.delegate as? AppDelegate else { return }
238+
if let indexedRouteResponse = indexedRouteResponse {
239+
delegate.carPlayManager.previewRoutes(for: indexedRouteResponse)
240+
} else {
241+
delegate.carPlayManager.cancelRoutesPreview()
242+
}
229243
}
230244

231245
func requestNotificationCenterAuthorization() {
@@ -241,7 +255,7 @@ class ViewController: UIViewController {
241255
}
242256

243257
@IBAction func clearMapPressed(_ sender: Any) {
244-
clearNavigationMapView()
258+
indexedRouteResponse = nil
245259
}
246260

247261
@IBAction func startButtonPressed(_ sender: Any) {
@@ -598,7 +612,6 @@ class ViewController: UIViewController {
598612
self.waypoints = waypoints
599613
}
600614

601-
self.clearMap.isHidden = false
602615
self.longPressHintView.isHidden = true
603616
}
604617

Sources/MapboxNavigation/CPMapTemplate.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,10 @@ extension CLLocationDirection {
3131
}
3232
}
3333

34+
extension CPTemplate {
35+
var currentActivity: CarPlayActivity? {
36+
guard let userInfo = userInfo as? CarPlayUserInfo else { return nil }
37+
return userInfo[CarPlayManager.currentActivityKey] as? CarPlayActivity
38+
}
39+
}
40+

Sources/MapboxNavigation/CPRouteChoice.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@ extension CPRouteChoice {
2121

2222
return userInfo[IndexedRouteResponseUserInfo.key] as? IndexedRouteResponseUserInfo
2323
}
24+
25+
public var indexedRouteResponse: IndexedRouteResponse? {
26+
return indexedRouteResponseUserInfo?.indexedRouteResponse
27+
}
2428
}

Sources/MapboxNavigation/CPTrip.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ extension CPTrip {
1414
waypoints = matchOptions.waypoints
1515
}
1616

17-
let routeChoices = indexedRouteResponse.routeResponse.routes?.enumerated().map { (routeIndex, route) -> CPRouteChoice in
17+
var routeChoices = indexedRouteResponse.routeResponse.routes?.enumerated().map { (routeIndex, route) -> CPRouteChoice in
1818
let summaryVariants = [
1919
DateComponentsFormatter.fullDateComponentsFormatter.string(from: route.expectedTravelTime)!,
2020
DateComponentsFormatter.shortDateComponentsFormatter.string(from: route.expectedTravelTime)!,
@@ -33,6 +33,9 @@ extension CPTrip {
3333
return routeChoice
3434
} ?? []
3535

36+
// The selected route within the `IndexedRouteResponse` will default to be the first in route choice.
37+
routeChoices.insert(routeChoices.remove(at: indexedRouteResponse.routeIndex), at: 0)
38+
3639
guard let originCoordinate = waypoints.first?.coordinate,
3740
let destinationCoordinate = waypoints.last?.coordinate else {
3841
preconditionFailure("Origin and destination coordinates should be valid.")

Sources/MapboxNavigation/CarPlayManager.swift

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ public class CarPlayManager: NSObject {
9090

9191
private weak var navigationService: NavigationService?
9292
private var idleTimerCancellable: IdleTimerManager.Cancellable?
93+
private var indexedRouteResponse: IndexedRouteResponse?
9394

9495
/**
9596
Programatically begins a CarPlay turn-by-turn navigation session.
@@ -493,8 +494,7 @@ extension CarPlayManager: CPInterfaceControllerDelegate {
493494
carPlayMapViewController.recenterButton.isHidden = true
494495
}
495496

496-
if let userInfo = template.userInfo as? Dictionary<String, Any>,
497-
let currentActivity = userInfo[CarPlayManager.currentActivityKey] as? CarPlayActivity {
497+
if let currentActivity = template.currentActivity {
498498
self.currentActivity = currentActivity
499499
} else {
500500
self.currentActivity = nil
@@ -505,13 +505,8 @@ extension CarPlayManager: CPInterfaceControllerDelegate {
505505
delegate?.carPlayManager(self, templateDidAppear: template, animated: animated)
506506

507507
guard interfaceController?.topTemplate == mainMapTemplate,
508-
template == interfaceController?.rootTemplate,
509-
let carPlayMapViewController = carPlayMapViewController else { return }
510-
511-
let navigationMapView = carPlayMapViewController.navigationMapView
512-
navigationMapView.removeRoutes()
513-
navigationMapView.removeContinuousAlternativesRoutes()
514-
navigationMapView.removeWaypoints()
508+
template == interfaceController?.rootTemplate else { return }
509+
self.removeRoutesFromMap()
515510
}
516511

517512
public func templateWillDisappear(_ template: CPTemplate, animated: Bool) {
@@ -521,7 +516,7 @@ extension CarPlayManager: CPInterfaceControllerDelegate {
521516
let topTemplate = interfaceController.topTemplate,
522517
type(of: topTemplate) == CPSearchTemplate.self ||
523518
interfaceController.templates.count == 1 else { return }
524-
519+
525520
navigationMapView?.navigationCamera.follow()
526521
}
527522

@@ -614,10 +609,27 @@ extension CarPlayManager {
614609
previewed.
615610
*/
616611
public func previewRoutes(for indexedRouteResponse: IndexedRouteResponse) {
612+
guard shouldPreviewRoutes(for: indexedRouteResponse) else { return }
617613
let trip = CPTrip(indexedRouteResponse: indexedRouteResponse)
618614
previewRoutes(for: trip)
619615
}
620616

617+
/**
618+
Allows to cancel routes preview on CarPlay .
619+
*/
620+
public func cancelRoutesPreview() {
621+
guard self.indexedRouteResponse != nil else { return }
622+
self.indexedRouteResponse = nil
623+
mainMapTemplate?.hideTripPreviews()
624+
popToRootTemplate(interfaceController: interfaceController, animated: true)
625+
delegate?.carPlayManagerDidCancelPreview(self)
626+
}
627+
628+
func shouldPreviewRoutes(for indexedRouteResponse: IndexedRouteResponse) -> Bool {
629+
guard self.indexedRouteResponse?.currentRoute == indexedRouteResponse.currentRoute else { return true }
630+
return self.indexedRouteResponse?.routeResponse.routes != indexedRouteResponse.routeResponse.routes
631+
}
632+
621633
func previewRoutes(for trip: CPTrip) {
622634
guard let traitCollection = (self.carWindow?.rootViewController as? CarPlayMapViewController)?.traitCollection,
623635
let interfaceController = interfaceController else {
@@ -635,8 +647,23 @@ extension CarPlayManager {
635647
previewText = customPreviewText
636648
}
637649

650+
previewMapTemplate.backButton = defaultTripPreviewBackButton()
638651
previewMapTemplate.showTripPreviews([modifiedTrip], textConfiguration: previewText)
639-
interfaceController.pushTemplate(previewMapTemplate, animated: true)
652+
653+
if currentActivity == .previewing {
654+
interfaceController.popTemplate(animated: false)
655+
interfaceController.pushTemplate(previewMapTemplate, animated: false)
656+
} else {
657+
interfaceController.pushTemplate(previewMapTemplate, animated: true)
658+
}
659+
}
660+
661+
func removeRoutesFromMap() {
662+
indexedRouteResponse = nil
663+
guard let navigationMapView = carPlayMapViewController?.navigationMapView else { return }
664+
navigationMapView.removeRoutes()
665+
navigationMapView.removeContinuousAlternativesRoutes()
666+
navigationMapView.removeWaypoints()
640667
}
641668

642669
func calculate(_ options: RouteOptions, completionHandler: @escaping RoutingProvider.IndexedRouteResponseCompletionHandler) {
@@ -690,6 +717,18 @@ extension CarPlayManager {
690717
overviewButtonTitle: overviewTitle)
691718
return defaultPreviewText
692719
}
720+
721+
private func defaultTripPreviewBackButton() -> CPBarButton {
722+
let backButton = CPBarButton(type: .text) { [weak self] (button: CPBarButton) in
723+
guard let self = self else { return }
724+
self.cancelRoutesPreview()
725+
}
726+
backButton.title = NSLocalizedString("CARPLAY_PREVIEW_BACK",
727+
bundle: .mapboxNavigation,
728+
value: "BACK",
729+
comment: "Title for trip preview back button")
730+
return backButton
731+
}
693732
}
694733

695734
// MARK: CPMapTemplateDelegate Methods
@@ -704,7 +743,7 @@ extension CarPlayManager: CPMapTemplateDelegate {
704743
return
705744
}
706745

707-
guard let indexedRouteResponse = routeChoice.indexedRouteResponseUserInfo?.indexedRouteResponse else {
746+
guard let indexedRouteResponse = routeChoice.indexedRouteResponse else {
708747
preconditionFailure("CPRouteChoice should contain `IndexedRouteResponseUserInfo` struct.")
709748
}
710749

@@ -759,10 +798,7 @@ extension CarPlayManager: CPMapTemplateDelegate {
759798
self.delegate?.carPlayManager(self, didPresent: carPlayNavigationViewController)
760799
}
761800

762-
let navigationMapView = carPlayMapViewController.navigationMapView
763-
navigationMapView.removeRoutes()
764-
navigationMapView.removeContinuousAlternativesRoutes()
765-
navigationMapView.removeWaypoints()
801+
self.removeRoutesFromMap()
766802
}
767803

768804
func navigationMapTemplate() -> CPMapTemplate {
@@ -784,7 +820,7 @@ extension CarPlayManager: CPMapTemplateDelegate {
784820
using routeChoice: CPRouteChoice) {
785821
guard let carPlayMapViewController = carPlayMapViewController else { return }
786822

787-
guard let indexedRouteResponse = routeChoice.indexedRouteResponseUserInfo?.indexedRouteResponse,
823+
guard let indexedRouteResponse = routeChoice.indexedRouteResponse,
788824
let route = indexedRouteResponse.currentRoute,
789825
var routes = indexedRouteResponse.routeResponse.routes else {
790826
preconditionFailure("CPRouteChoice should contain `IndexedRouteResponseUserInfo` struct.")
@@ -803,7 +839,7 @@ extension CarPlayManager: CPMapTemplateDelegate {
803839
navigationMapView.showcase(routes,
804840
routesPresentationStyle: .all(shouldFit: true, cameraOptions: cameraOptions),
805841
animated: true)
806-
842+
self.indexedRouteResponse = indexedRouteResponse
807843
delegate?.carPlayManager(self, selectedPreviewFor: trip, using: routeChoice)
808844
}
809845

@@ -812,9 +848,7 @@ extension CarPlayManager: CPMapTemplateDelegate {
812848
return
813849
}
814850
let navigationMapView = carPlayMapViewController.navigationMapView
815-
navigationMapView.removeRoutes()
816-
navigationMapView.removeContinuousAlternativesRoutes()
817-
navigationMapView.removeWaypoints()
851+
self.removeRoutesFromMap()
818852
if let passiveLocationProvider = navigationMapView.mapView.location.locationProvider as? PassiveLocationProvider {
819853
passiveLocationProvider.locationManager.resumeTripSession()
820854
carPlayMapViewController.subscribeForFreeDriveNotifications()
@@ -889,10 +923,7 @@ extension CarPlayManager: CPMapTemplateDelegate {
889923
}
890924

891925
public func mapTemplateDidDismissPanningInterface(_ mapTemplate: CPMapTemplate) {
892-
guard let userInfo = mapTemplate.userInfo as? CarPlayUserInfo,
893-
let currentActivity = userInfo[CarPlayManager.currentActivityKey] as? CarPlayActivity else {
894-
return
895-
}
926+
guard let currentActivity = mapTemplate.currentActivity else { return }
896927

897928
self.currentActivity = currentActivity
898929

@@ -993,10 +1024,7 @@ extension CarPlayManager: CPMapTemplateDelegate {
9931024
- parameter mapTemplate: `CPMapTemplate` instance, for which buttons update will be performed.
9941025
*/
9951026
private func updateNavigationButtons(for mapTemplate: CPMapTemplate) {
996-
guard let userInfo = mapTemplate.userInfo as? CarPlayUserInfo,
997-
let currentActivity = userInfo[CarPlayManager.currentActivityKey] as? CarPlayActivity else {
998-
return
999-
}
1027+
guard let currentActivity = mapTemplate.currentActivity else { return }
10001028

10011029
let traitCollection: UITraitCollection
10021030
if let carPlayNavigationViewController = carPlayNavigationViewController {
@@ -1268,6 +1296,7 @@ extension CarPlayManager {
12681296
idleTimerCancellable = nil
12691297

12701298
unsubscribeFromNotifications()
1299+
indexedRouteResponse = nil
12711300
}
12721301
}
12731302

Sources/MapboxNavigation/CarPlayManagerDelegate.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ public protocol CarPlayManagerDelegate: AnyObject, UnimplementedLogging, CarPlay
113113
selectedPreviewFor trip: CPTrip,
114114
using routeChoice: CPRouteChoice)
115115

116+
/**
117+
Called when CarPlay canceled routes preview.
118+
This delegate method will be called after canceled the routes preview.
119+
120+
- parameter carPlayManager: The CarPlay manager instance.
121+
*/
122+
func carPlayManagerDidCancelPreview(_ carPlayManager: CarPlayManager)
123+
116124
// MARK: Monitoring Route Progress and Updates
117125

118126
/**
@@ -519,6 +527,13 @@ public extension CarPlayManagerDelegate {
519527
logUnimplemented(protocolType: CarPlayManagerDelegate.self, level: .debug)
520528
}
521529

530+
/**
531+
`UnimplementedLogging` prints a warning to standard output the first time this method is called.
532+
*/
533+
func carPlayManagerDidCancelPreview(_ carPlayManager: CarPlayManager) {
534+
logUnimplemented(protocolType: CarPlayManagerDelegate.self, level: .debug)
535+
}
536+
522537
/**
523538
`UnimplementedLogging` prints a warning to standard output the first time this method is called.
524539
*/

Sources/MapboxNavigation/CarPlayMapViewController.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ open class CarPlayMapViewController: UIViewController {
354354
// Trigger update of view constraints to correctly position views like `SpeedLimitView`.
355355
view.setNeedsUpdateConstraints()
356356

357-
guard let activeRoute = navigationMapView.routes?.first else {
357+
guard let routes = navigationMapView.routes, !routes.isEmpty else {
358358
navigationMapView.navigationCamera.follow()
359359
return
360360
}
@@ -364,7 +364,7 @@ open class CarPlayMapViewController: UIViewController {
364364
cameraOptions.pitch = 0
365365
navigationMapView.mapView.mapboxMap.setCamera(to: cameraOptions)
366366

367-
navigationMapView.fitCamera(to: [activeRoute])
367+
navigationMapView.fitCamera(to: routes)
368368
}
369369
}
370370

0 commit comments

Comments
 (0)