From 2c6ec67258dfc1902951845635465276ce4af261 Mon Sep 17 00:00:00 2001 From: Bobby Sudekum Date: Fri, 23 Jun 2017 20:48:02 +0200 Subject: [PATCH] Separate custom UI example (#301) * Seperate custom UI example * remove * use toolbar * Reorganize examples * Tweaks * Weak self * Set default camera to route origin + bearing * defaultCamera -> tiltedCamera --- Examples/Swift/Base.lproj/Main.storyboard | 273 +++++++++++--- Examples/Swift/CustomViewController.swift | 198 ++++++++++ Examples/Swift/ViewController.swift | 346 +++++++++--------- .../WaypointConfirmationViewController.swift | 14 + MapboxNavigation.xcodeproj/project.pbxproj | 8 + MapboxNavigation/RouteMapViewController.swift | 26 +- 6 files changed, 623 insertions(+), 242 deletions(-) create mode 100644 Examples/Swift/CustomViewController.swift create mode 100644 Examples/Swift/WaypointConfirmationViewController.swift diff --git a/Examples/Swift/Base.lproj/Main.storyboard b/Examples/Swift/Base.lproj/Main.storyboard index dfad6c84563..7b1472878be 100644 --- a/Examples/Swift/Base.lproj/Main.storyboard +++ b/Examples/Swift/Base.lproj/Main.storyboard @@ -1,15 +1,15 @@ - + - + - + @@ -23,53 +23,8 @@ - - - - - - - - - - - - - - - - @@ -87,22 +42,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - + + @@ -112,15 +121,163 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + + + + + + + + + + + + + + + + + + - + diff --git a/Examples/Swift/CustomViewController.swift b/Examples/Swift/CustomViewController.swift new file mode 100644 index 00000000000..1fc42c86c8e --- /dev/null +++ b/Examples/Swift/CustomViewController.swift @@ -0,0 +1,198 @@ +import UIKit +import MapboxCoreNavigation +import Mapbox +import CoreLocation +import AVFoundation +import MapboxDirections + +class CustomViewController: UIViewController, MGLMapViewDelegate, AVSpeechSynthesizerDelegate { + + var destination: MGLPointAnnotation! + let directions = Directions.shared + var routeController: RouteController! + + let distanceFormatter = MGLDistanceFormatter() + lazy var speechSynth = AVSpeechSynthesizer() + var userRoute: Route? + + @IBOutlet var mapView: MGLMapView! + @IBOutlet weak var arrowView: UILabel! + @IBOutlet weak var instructionLabel: UILabel! + @IBOutlet weak var distanceLabel: UILabel! + @IBOutlet weak var cancelButton: UIButton! + + override func viewDidLoad() { + super.viewDidLoad() + + mapView.delegate = self + + distanceFormatter.unitStyle = .long + distanceFormatter.numberFormatter.maximumFractionDigits = 0 + + routeController = RouteController(along: userRoute!, directions: directions) + routeController.snapsUserLocationAnnotationToRoute = true + + mapView.userLocationVerticalAlignment = .center + mapView.userTrackingMode = .followWithCourse + + resumeNotifications() + + // Start navigation + routeController.resume() + } + + deinit { + suspendNotifications() + routeController.suspendLocationUpdates() + } + + func resumeNotifications() { + NotificationCenter.default.addObserver(self, selector: #selector(alertLevelDidChange(_ :)), name: RouteControllerAlertLevelDidChange, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_ :)), name: RouteControllerProgressDidChange, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(rerouted(_:)), name: RouteControllerWillReroute, object: nil) + } + + func suspendNotifications() { + NotificationCenter.default.removeObserver(self, name: RouteControllerAlertLevelDidChange, object: nil) + NotificationCenter.default.removeObserver(self, name: RouteControllerProgressDidChange, object: nil) + NotificationCenter.default.removeObserver(self, name: RouteControllerWillReroute, object: nil) + } + + func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) { + addRouteToMap() + } + + // When the alert level changes, this signals the user is ready for a voice announcement + func alertLevelDidChange(_ notification: NSNotification) { + let routeProgress = notification.userInfo![RouteControllerAlertLevelDidChangeNotificationRouteProgressKey] as! RouteProgress + let alertLevel = routeProgress.currentLegProgress.alertUserLevel + var text: String + + let distance = routeProgress.currentLegProgress.currentStepProgress.distanceRemaining + let formattedDistance = distanceFormatter.string(fromDistance: distance) + if let upComingStep = routeProgress.currentLegProgress.upComingStep { + // Don't give full instruction with distance if the alert type is high + if alertLevel == .high { + text = upComingStep.instructions + } else { + text = "In \(formattedDistance) \(upComingStep.instructions)" + } + } else { + text = "In \(formattedDistance) \(routeProgress.currentLegProgress.currentStep.instructions)" + } + + let utterance = AVSpeechUtterance(string: text) + speechSynth.delegate = self + speechSynth.speak(utterance) + } + + // Notifications sent on all location updates + func progressDidChange(_ notification: NSNotification) { + let routeProgress = notification.userInfo![RouteControllerAlertLevelDidChangeNotificationRouteProgressKey] as! RouteProgress + updateRouteProgress(routeProgress: routeProgress) + } + + // Updates the turn banner with information about the next turn + func updateRouteProgress(routeProgress: RouteProgress) { + guard let step = routeProgress.currentLegProgress.upComingStep else { return } + + if let direction = step.maneuverDirection { + switch direction { + case .slightRight: + self.arrowView.text = "↗️" + case .sharpRight, .right: + self.arrowView.text = "➡️" + case .slightLeft: + self.arrowView.text = "↖️" + case .sharpLeft, .left: + self.arrowView.text = "⬅️" + case .uTurn: + self.arrowView.text = "⤵️" + default: + self.arrowView.text = "⬆️" + } + } + self.instructionLabel.text = step.destinationCodes?.first ?? step.destinations?.first ?? step.names?.first ?? step.instructions + let distance = routeProgress.currentLegProgress.currentStepProgress.distanceRemaining + self.distanceLabel.text = distanceFormatter.string(fromMeters: distance) + } + + // Fired when the user is no longer on the route. + // A new route should be fetched at this time. + func rerouted(_ notification: NSNotification) { + speechSynth.stopSpeaking(at: .word) + + getRoute { + /* + **IMPORTANT** + + When rerouting, you need to give the RouteController a new route. + Otherwise, it will continue to compare the user to the old route and continually reroute the user. + */ + let routeProgress = RouteProgress(route: self.userRoute!) + self.routeController.routeProgress = routeProgress + self.updateRouteProgress(routeProgress: routeProgress) + } + } + + func getRoute(completion: (()->())? = nil) { + let options = RouteOptions(coordinates: [mapView.userLocation!.coordinate, destination.coordinate]) + options.includesSteps = true + options.routeShapeResolution = .full + options.profileIdentifier = .automobileAvoidingTraffic + + _ = directions.calculate(options) { [weak self] (waypoints, routes, error) in + guard error == nil else { + print(error!) + return + } + + guard let route = routes?.first else { + return + } + + self?.userRoute = route + + completion?() + self?.addRouteToMap() + } + } + + func addRouteToMap() { + guard let style = mapView.style else { return } + guard let userRoute = userRoute else { return } + + if let annotations = mapView.annotations { + mapView.removeAnnotations(annotations) + } + mapView.addAnnotation(destination) + + let polyline = MGLPolylineFeature(coordinates: userRoute.coordinates!, count: userRoute.coordinateCount) + let lineSource = MGLShapeSource(identifier: sourceIdentifier, shape: polyline, options: nil) + + if let source = style.source(withIdentifier: sourceIdentifier) as? MGLShapeSource { + source.shape = polyline + } else { + let line = MGLLineStyleLayer(identifier: layerIdentifier, source: lineSource) + + // Style the line + line.lineColor = MGLStyleValue(rawValue: UIColor(red:0.00, green:0.45, blue:0.74, alpha:0.9)) + line.lineWidth = MGLStyleValue(rawValue: 5) + line.lineCap = MGLStyleValue(rawValue: NSValue(mglLineCap: .round)) + line.lineJoin = MGLStyleValue(rawValue: NSValue(mglLineJoin: .round)) + + // Add source and layer + style.addSource(lineSource) + for layer in style.layers.reversed() { + if !(layer is MGLSymbolStyleLayer) { + style.insertLayer(line, above: layer) + break + } + } + } + } + + @IBAction func cancelButtonPressed(_ sender: Any) { + self.dismiss(animated: true, completion: nil) + } +} diff --git a/Examples/Swift/ViewController.swift b/Examples/Swift/ViewController.swift index eeb03636d7e..c6263f52d2f 100644 --- a/Examples/Swift/ViewController.swift +++ b/Examples/Swift/ViewController.swift @@ -7,33 +7,48 @@ import Mapbox let sourceIdentifier = "sourceIdentifier" let layerIdentifier = "layerIdentifier" -class ViewController: UIViewController, MGLMapViewDelegate, NavigationViewControllerDelegate, NavigationMapViewDelegate { +enum ExampleMode { + case `default` + case custom + case styled + case multipleWaypoints +} + +class ViewController: UIViewController, MGLMapViewDelegate { var destination: MGLPointAnnotation? - var navigation: RouteController? - var userRoute: Route? + var currentRoute: Route? { + didSet { + self.startButton.isEnabled = currentRoute != nil + } + } @IBOutlet weak var mapView: NavigationMapView! - @IBOutlet weak var startNavigationButton: UIButton! - @IBOutlet weak var simulateNavigationButton: UIButton! - @IBOutlet weak var howToBeginLabel: UILabel! + @IBOutlet weak var longPressHintView: UIView! + + @IBOutlet weak var simulationButton: UIButton! + @IBOutlet weak var startButton: UIButton! + var exampleMode: ExampleMode? + var nextWaypoint: CLLocationCoordinate2D? override func viewDidLoad() { super.viewDidLoad() automaticallyAdjustsScrollViewInsets = false - mapView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 44, right: 0) mapView.delegate = self - mapView.navigationMapDelegate = self mapView.userTrackingMode = .follow - resumeNotifications() + + simulationButton.isSelected = true + startButton.isEnabled = false } - deinit { - suspendNotifications() - navigation?.suspendLocationUpdates() + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // Reset the navigation styling to the defaults + Style.defaultStyle.apply() } @IBAction func didLongPress(_ sender: UILongPressGestureRecognizer) { @@ -45,68 +60,45 @@ class ViewController: UIViewController, MGLMapViewDelegate, NavigationViewContro mapView.removeAnnotation(destination) } + longPressHintView.isHidden = true + destination = MGLPointAnnotation() destination?.coordinate = mapView.convert(sender.location(in: mapView), toCoordinateFrom: mapView) mapView.addAnnotation(destination!) - getRoute() - } - - @IBAction func didTapStartNavigation(_ sender: Any) { - startNavigation(along: userRoute!) - } - - @IBAction func didTapSimulateNavigation(_ sender: Any) { - startNavigation(along: userRoute!, simulatesLocationUpdates: true) - } - - func resumeNotifications() { - NotificationCenter.default.addObserver(self, selector: #selector(alertLevelDidChange(_ :)), name: RouteControllerAlertLevelDidChange, object: navigation) - NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_ :)), name: RouteControllerProgressDidChange, object: navigation) - NotificationCenter.default.addObserver(self, selector: #selector(willReroute(_:)), name: RouteControllerWillReroute, object: navigation) + requestRoute() } - func suspendNotifications() { - NotificationCenter.default.removeObserver(self, name: RouteControllerAlertLevelDidChange, object: navigation) - NotificationCenter.default.removeObserver(self, name: RouteControllerProgressDidChange, object: navigation) - NotificationCenter.default.removeObserver(self, name: RouteControllerWillReroute, object: navigation) + @IBAction func simulateButtonPressed(_ sender: Any) { + simulationButton.isSelected = !simulationButton.isSelected } - // Notification sent when the alert level changes. - func alertLevelDidChange(_ notification: NSNotification) { - // Good place to give alerts about maneuver. These announcements are handled by `RouteVoiceController` + @IBAction func startButtonPressed(_ sender: Any) { + let alertController = UIAlertController(title: "Start Navigation", message: "Select the navigation type", preferredStyle: .actionSheet) + alertController.addAction(UIAlertAction(title: "Default UI", style: .default, handler: { (action) in + self.startBasicNavigation() + })) + alertController.addAction(UIAlertAction(title: "Custom UI", style: .default, handler: { (action) in + self.startCustomNavigation() + })) + alertController.addAction(UIAlertAction(title: "Styled UI", style: .default, handler: { (action) in + self.startStyledNavigation() + })) + alertController.addAction(UIAlertAction(title: "Multiple Stops", style: .default, handler: { (action) in + self.startMultipleWaypoints() + })) + alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + present(alertController, animated: true, completion: nil) } - // Notifications sent on all location updates - func progressDidChange(_ notification: NSNotification) { - // If you are using MapboxCoreNavigation, - // this would be a good time to update UI elements. - // You can grab the current routeProgress like: - // let routeProgress = notification.userInfo![RouteControllerAlertLevelDidChangeNotificationRouteProgressKey] as! RouteProgress - } - - // Notification sent when the user is determined to be off the current route - func willReroute(_ notification: NSNotification) { - // - // If you're using MapboxNavigation, - // this is how you'd handle fetching a new route and setting it as the active route - /* - getRoute { - /* - **IMPORTANT** - - When rerouting, you need to give the RouteController a new route. - Otherwise, it will continue to compare the user to the old route and continually reroute the user. - */ - self.navigation?.routeProgress = RouteProgress(route: self.userRoute!) - } - */ - } - - func getRoute(didFinish: (()->())? = nil) { + // Helper for requesting a route + func requestRoute() { guard let destination = destination else { return } - let options = RouteOptions(coordinates: [mapView.userLocation!.coordinate, destination.coordinate]) + let options = RouteOptions(coordinates: [ + mapView.userLocation!.coordinate, + destination.coordinate, + ]) options.includesSteps = true options.routeShapeResolution = .full options.profileIdentifier = .automobileAvoidingTraffic @@ -116,61 +108,59 @@ class ViewController: UIViewController, MGLMapViewDelegate, NavigationViewContro print(error!) return } - guard let route = routes?.first else { - return - } - - self?.userRoute = route - self?.startNavigationButton.isHidden = false - self?.simulateNavigationButton.isHidden = false - self?.howToBeginLabel.isHidden = true + guard let route = routes?.first else { return } + + self?.currentRoute = route // Open method for adding and updating the route line self?.mapView.showRoute(route) - - didFinish?() } } - func startNavigation(along route: Route, simulatesLocationUpdates: Bool = false) { - // Pass through: - // 1. The route the user will take. - // 2. A `Directions` object, used for rerouting. + // MARK: - Basic Navigation + + func startBasicNavigation() { + guard let route = currentRoute else { return } + + exampleMode = .default + let navigationViewController = NavigationViewController(for: route) - - // If you'd like to use AWS Polly, provide your IdentityPoolId below. - // `identityPoolId` is a required value for using AWS Polly voice instead of iOS's built in AVSpeechSynthesizer. - // You can get a token here: http://docs.aws.amazon.com/mobile/sdkforios/developerguide/cognito-auth-aws-identity-for-ios.html - //navigationViewController.voiceController?.identityPoolId = "<#Your AWS IdentityPoolId. Remove Argument if you do not want to use AWS Polly#>" - - navigationViewController.simulatesLocationUpdates = simulatesLocationUpdates - navigationViewController.routeController.snapsUserLocationAnnotationToRoute = true - navigationViewController.voiceController?.volume = 0.5 + navigationViewController.simulatesLocationUpdates = simulationButton.isSelected navigationViewController.navigationDelegate = self - - // Uncomment to apply custom styles -// styleForRegular().apply() -// styleForCompact().apply() -// styleForiPad().apply() -// styleForCarPlay().apply() - - let camera = mapView.camera - camera.pitch = 45 - camera.altitude = 600 - if let userLocation = mapView.userLocation { - camera.centerCoordinate = userLocation.coordinate - if let location = userLocation.location { - camera.heading = location.course - } - } - navigationViewController.pendingCamera = camera - + present(navigationViewController, animated: true, completion: nil) } + + // MARK: - Custom Navigation UI + + func startCustomNavigation() { + guard let route = self.currentRoute else { return } + + guard let customViewController = self.storyboard?.instantiateViewController(withIdentifier: "custom") as? CustomViewController else { return } + + exampleMode = .custom + + customViewController.userRoute = route + customViewController.destination = self.destination + + present(customViewController, animated: true, completion: nil) + } - func styleForRegular() -> Style { - let trait = UITraitCollection(verticalSizeClass: .regular) - let style = Style(traitCollection: trait) + // MARK: - Styling the default UI + + func startStyledNavigation() { + guard let route = self.currentRoute else { return } + + exampleMode = .styled + + let navigationViewController = NavigationViewController(for: route) + navigationViewController.simulatesLocationUpdates = simulationButton.isSelected + navigationViewController.navigationDelegate = self + + // Set a custom style URL + navigationViewController.mapView?.styleURL = URL(string: "mapbox://styles/mapbox/navigation-guidance-day-v1") + + let style = Style() // General styling style.tintColor = #colorLiteral(red: 0.9418798089, green: 0.3469682932, blue: 0.5911870599, alpha: 1) @@ -178,87 +168,95 @@ class ViewController: UIViewController, MGLMapViewDelegate, NavigationViewContro style.secondaryTextColor = #colorLiteral(red: 0.9626983484, green: 0.9626983484, blue: 0.9626983484, alpha: 1) style.buttonTextColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) - style.wayNameTextColor = #colorLiteral(red: 0.9418798089, green: 0.3469682932, blue: 0.5911870599, alpha: 1) - // Maneuver view (Page view) style.maneuverViewBackgroundColor = #colorLiteral(red: 0.2974345386, green: 0.4338284135, blue: 0.9865127206, alpha: 1) style.maneuverViewHeight = 100 - + + // Current street name label + style.wayNameTextColor = #colorLiteral(red: 0.9418798089, green: 0.3469682932, blue: 0.5911870599, alpha: 1) + // Table view (Drawer) style.headerBackgroundColor = #colorLiteral(red: 0.2974345386, green: 0.4338284135, blue: 0.9865127206, alpha: 1) - style.cellTitleLabelTextColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) - style.cellSubtitleLabelTextColor = #colorLiteral(red: 0.9626983484, green: 0.9626983484, blue: 0.9626983484, alpha: 1) - style.cellTitleLabelFont = UIFont.preferredFont(forTextStyle: .headline) - style.cellSubtitleLabelFont = UIFont.preferredFont(forTextStyle: .footnote) - - return style - } - - func styleForCompact() -> Style { - let horizontal = UITraitCollection(horizontalSizeClass: .compact) - let vertical = UITraitCollection(verticalSizeClass: .compact) - let traitCollection = UITraitCollection(traitsFrom: [horizontal, vertical]) - let style = Style(traitCollection: traitCollection) - - // General styling - style.tintColor = #colorLiteral(red: 0.2974345386, green: 0.4338284135, blue: 0.9865127206, alpha: 1) - style.primaryTextColor = .black - style.secondaryTextColor = .gray - style.buttonTextColor = .black - - // Maneuver view (Page view) - style.maneuverViewBackgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) - style.maneuverViewHeight = 70 - - // Table view (Drawer) - style.headerBackgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) - style.primaryTextColor = .black - style.secondaryTextColor = .gray - style.cellTitleLabelTextColor = .black - style.cellSubtitleLabelTextColor = .gray - style.cellTitleLabelFont = .preferredFont(forTextStyle: .headline) - style.cellSubtitleLabelFont = .preferredFont(forTextStyle: .footnote) - - return style - } - - func styleForiPad() -> Style { - let style = Style(traitCollection: UITraitCollection(userInterfaceIdiom: .pad)) - style.maneuverViewHeight = 100 - return style - } - - func styleForCarPlay() -> Style { - let style = Style(traitCollection: UITraitCollection(userInterfaceIdiom: .carPlay)) - style.maneuverViewHeight = 40 - return style + style.cellTitleLabelTextColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1) + style.cellSubtitleLabelTextColor = #colorLiteral(red: 0.2549019754, green: 0.2745098174, blue: 0.3019607961, alpha: 1) + style.cellTitleLabelFont = UIFont(name: "Georgia-Bold", size: 17) + style.cellSubtitleLabelFont = UIFont(name: "Georgia", size: 15) + + style.apply() + + // If you'd like to use AWS Polly, provide your IdentityPoolId below. + // `identityPoolId` is a required value for using AWS Polly voice instead of iOS's built in AVSpeechSynthesizer. + // You can get a token here: http://docs.aws.amazon.com/mobile/sdkforios/developerguide/cognito-auth-aws-identity-for-ios.html + //navigationViewController.voiceController?.identityPoolId = "<#Your AWS IdentityPoolId. Remove Argument if you do not want to use AWS Polly#>" + + present(navigationViewController, animated: true, completion: nil) } - /// Delegate method for changing the route line style - func navigationMapView(_ mapView: NavigationMapView, routeStyleLayerWithIdentifier identifier: String, source: MGLSource) -> MGLStyleLayer? { - let lineCasing = MGLLineStyleLayer(identifier: identifier, source: source) + // MARK: - Navigation with multiple waypoints + + func startMultipleWaypoints() { + guard let route = self.currentRoute else { return } + + exampleMode = .multipleWaypoints - lineCasing.lineColor = MGLStyleValue(rawValue: UIColor(red:0.00, green:0.70, blue:0.99, alpha:1.0)) - lineCasing.lineWidth = MGLStyleValue(rawValue: 6) + // When the user arrives at their destination, we'll prompt them to return back to where they started + nextWaypoint = self.currentRoute?.coordinates?.first - lineCasing.lineCap = MGLStyleValue(rawValue: NSValue(mglLineCap: .round)) - lineCasing.lineJoin = MGLStyleValue(rawValue: NSValue(mglLineJoin: .round)) - return lineCasing + let navigationViewController = NavigationViewController(for: route) + navigationViewController.simulatesLocationUpdates = simulationButton.isSelected + navigationViewController.navigationDelegate = self + + present(navigationViewController, animated: true, completion: nil) } - /// Delegate method for changing the route line casing style - func navigationMapView(_ mapView: NavigationMapView, routeCasingStyleLayerWithIdentifier identifier: String, source: MGLSource) -> MGLStyleLayer? { - let line = MGLLineStyleLayer(identifier: identifier, source: source) +} + +extension ViewController: NavigationViewControllerDelegate { + func navigationViewController(_ navigationViewController: NavigationViewController, didArriveAt destination: MGLAnnotation) { - line.lineColor = MGLStyleValue(rawValue: UIColor(red:0.18, green:0.49, blue:0.78, alpha:1.0)) - line.lineWidth = MGLStyleValue(rawValue: 8) + // Multiple waypoint demo + guard exampleMode == .multipleWaypoints, nextWaypoint != nil else { return } + + // When the user arrives, present a view controller that prompts the user to continue to their next destination + // This typ of screen could show information about a destination, pickup/dropoff confirmation, instructions upon arrival, etc. + guard let confirmationController = self.storyboard?.instantiateViewController(withIdentifier: "waypointConfirmation") as? WaypointConfirmationViewController else { return } + + confirmationController.delegate = self - line.lineCap = MGLStyleValue(rawValue: NSValue(mglLineCap: .round)) - line.lineJoin = MGLStyleValue(rawValue: NSValue(mglLineJoin: .round)) - return line + navigationViewController.present(confirmationController, animated: true, completion: nil) } - - func navigationViewController(_ navigationViewController: NavigationViewController, didArriveAt destination: MGLAnnotation) { - print("User arrived at \(destination)") +} + +extension ViewController: WaypointConfirmationViewControllerDelegate { + func confirmationControllerDidConfirm(controller confirmationController: WaypointConfirmationViewController) { + guard let nextDestination = nextWaypoint else { return } + guard let navigationViewController = self.presentedViewController as? NavigationViewController else { return } + + // Calculate directions to the next waypoint + let options = RouteOptions(coordinates: [ + navigationViewController.mapView!.userLocation!.coordinate, + nextDestination, + ]) + options.includesSteps = true + options.routeShapeResolution = .full + options.profileIdentifier = navigationViewController.route.routeOptions.profileIdentifier + + _ = Directions.shared.calculate(options) { [weak self] (waypoints, routes, error) in + guard error == nil else { + print(error!) + return + } + guard let route = routes?.first else { return } + + // update the navigationViewController with the route to the next waypoint + navigationViewController.route = route + + // Set the next waypoint to our start point + // We'll continue this waypoint loop until the user exits navigation + self?.nextWaypoint = route.coordinates?.first + + // Dismiss the confirmation screen + confirmationController.dismiss(animated: true, completion: nil) + } } } diff --git a/Examples/Swift/WaypointConfirmationViewController.swift b/Examples/Swift/WaypointConfirmationViewController.swift new file mode 100644 index 00000000000..126cab41c32 --- /dev/null +++ b/Examples/Swift/WaypointConfirmationViewController.swift @@ -0,0 +1,14 @@ +import UIKit + +protocol WaypointConfirmationViewControllerDelegate: NSObjectProtocol { + func confirmationControllerDidConfirm(controller: WaypointConfirmationViewController) +} + +class WaypointConfirmationViewController: UIViewController { + + weak var delegate: WaypointConfirmationViewControllerDelegate? + + @IBAction func continueButtonPressed(_ sender: Any) { + delegate?.confirmationControllerDidConfirm(controller: self) + } +} diff --git a/MapboxNavigation.xcodeproj/project.pbxproj b/MapboxNavigation.xcodeproj/project.pbxproj index bdc0d5e376d..a4e77e9b323 100644 --- a/MapboxNavigation.xcodeproj/project.pbxproj +++ b/MapboxNavigation.xcodeproj/project.pbxproj @@ -97,6 +97,7 @@ 35D825FC1E6A2DBE0088F83B /* MGLMapView+MGLNavigationAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 35D825FA1E6A2DBE0088F83B /* MGLMapView+MGLNavigationAdditions.m */; }; 35D825FE1E6A2EC60088F83B /* MapboxNavigation.h in Headers */ = {isa = PBXBuildFile; fileRef = 35D825FD1E6A2EC60088F83B /* MapboxNavigation.h */; settings = {ATTRIBUTES = (Public, ); }; }; 35EC316D1EA64057004280F5 /* SimulatedRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35EC316C1EA64057004280F5 /* SimulatedRoute.swift */; }; + 6441B16A1EFC64E50076499F /* WaypointConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6441B1691EFC64E50076499F /* WaypointConfirmationViewController.swift */; }; C5000A931EC25C6E00563EA9 /* Abbreviations.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5000A921EC25C6E00563EA9 /* Abbreviations.swift */; }; C520EE901EBB84F9008805BC /* Navigation.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C520EE921EBB84F9008805BC /* Navigation.storyboard */; }; C5212B551EC4BE97009538EB /* Abbreviations.plist in Resources */ = {isa = PBXBuildFile; fileRef = C5212B571EC4BE97009538EB /* Abbreviations.plist */; }; @@ -113,6 +114,7 @@ C5C94C1C1DDCD2340097296A /* RouteController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5ADFBF91DDCC9580011824B /* RouteController.swift */; }; C5C94C1D1DDCD2370097296A /* RouteProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5ADFBFB1DDCC9AD0011824B /* RouteProgress.swift */; }; C5C94C1E1DDCD23A0097296A /* Geometry.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5ADFBFD1DDCC9CE0011824B /* Geometry.swift */; }; + C5D9800D1EFA8BA9006DBF2E /* CustomViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5D9800C1EFA8BA9006DBF2E /* CustomViewController.swift */; }; DAAE5F301EAE4C4700832871 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DAAE5F321EAE4C4700832871 /* Localizable.strings */; }; DAB2CCE71DF7AFDF001B2FE1 /* dc-line.geojson in Resources */ = {isa = PBXBuildFile; fileRef = DAB2CCE61DF7AFDE001B2FE1 /* dc-line.geojson */; }; /* End PBXBuildFile section */ @@ -326,6 +328,7 @@ 35D825FA1E6A2DBE0088F83B /* MGLMapView+MGLNavigationAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MGLMapView+MGLNavigationAdditions.m"; sourceTree = ""; }; 35D825FD1E6A2EC60088F83B /* MapboxNavigation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MapboxNavigation.h; sourceTree = ""; }; 35EC316C1EA64057004280F5 /* SimulatedRoute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimulatedRoute.swift; sourceTree = ""; }; + 6441B1691EFC64E50076499F /* WaypointConfirmationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaypointConfirmationViewController.swift; sourceTree = ""; }; C5000A921EC25C6E00563EA9 /* Abbreviations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Abbreviations.swift; sourceTree = ""; }; C520EE911EBB84F9008805BC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Navigation.storyboard; sourceTree = ""; }; C520EE941EBBBD55008805BC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Navigation.strings; sourceTree = ""; }; @@ -347,6 +350,7 @@ C5ADFBF91DDCC9580011824B /* RouteController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RouteController.swift; sourceTree = ""; }; C5ADFBFB1DDCC9AD0011824B /* RouteProgress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RouteProgress.swift; sourceTree = ""; }; C5ADFBFD1DDCC9CE0011824B /* Geometry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Geometry.swift; sourceTree = ""; }; + C5D9800C1EFA8BA9006DBF2E /* CustomViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomViewController.swift; sourceTree = ""; }; DAAE5F311EAE4C4700832871 /* Base */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = ""; }; DAAE5F331EAE4C5A00832871 /* zh-Hans */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; DAB2CCE61DF7AFDE001B2FE1 /* dc-line.geojson */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "dc-line.geojson"; sourceTree = ""; }; @@ -537,6 +541,8 @@ 35002D721E5F6C830090E733 /* Supporting files */, 358D14651E5E3B7700ADE590 /* AppDelegate.swift */, 358D14671E5E3B7700ADE590 /* ViewController.swift */, + C5D9800C1EFA8BA9006DBF2E /* CustomViewController.swift */, + 6441B1691EFC64E50076499F /* WaypointConfirmationViewController.swift */, ); name = Swift; path = Examples/Swift; @@ -1156,6 +1162,8 @@ buildActionMask = 2147483647; files = ( 358D14681E5E3B7700ADE590 /* ViewController.swift in Sources */, + C5D9800D1EFA8BA9006DBF2E /* CustomViewController.swift in Sources */, + 6441B16A1EFC64E50076499F /* WaypointConfirmationViewController.swift in Sources */, 358D14661E5E3B7700ADE590 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index bec66b95346..056f3272fb4 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -34,6 +34,14 @@ class RouteMapViewController: UIViewController { } return parent.pendingCamera } + var tiltedCamera: MGLMapCamera { + get { + let camera = mapView.camera + camera.altitude = 600 + camera.pitch = 45 + return camera + } + } weak var delegate: RouteMapViewControllerDelegate? weak var routeController: RouteController! @@ -80,7 +88,12 @@ class RouteMapViewController: UIViewController { if let camera = pendingCamera { mapView.camera = camera } else { - setDefaultCamera(animated: false) + let camera = tiltedCamera + if let coordinates = route.coordinates, coordinates.count > 1 { + camera.centerCoordinate = coordinates.first! + camera.heading = coordinates[0].direction(to: coordinates[1]) + } + mapView.setCamera(camera, animated: false) } UIDevice.current.addObserver(self, forKeyPath: "batteryState", options: .initial, context: nil) @@ -119,7 +132,7 @@ class RouteMapViewController: UIViewController { } @IBAction func recenter(_ sender: AnyObject) { - setDefaultCamera(animated: false) + mapView.setCamera(tiltedCamera, animated: false) mapView.userTrackingMode = .followWithCourse // Recenter also resets the current page. Same behavior as rerouting. @@ -129,7 +142,7 @@ class RouteMapViewController: UIViewController { @IBAction func toggleOverview(_ sender: Any) { if isInOverviewMode { overviewButton.isHidden = false - setDefaultCamera(animated: false) + mapView.setCamera(tiltedCamera, animated: false) mapView.setUserTrackingMode(.followWithCourse, animated: true) } else { wayNameView.isHidden = true @@ -201,13 +214,6 @@ class RouteMapViewController: UIViewController { return navigationMapView(mapView, imageFor: annotation) } - func setDefaultCamera(animated: Bool) { - let camera = mapView.camera - camera.altitude = 600 - camera.pitch = 50 - mapView.setCamera(camera, animated: animated) - } - func notifyDidChange(routeProgress: RouteProgress, location: CLLocation, secondsRemaining: TimeInterval) { guard var controller = routePageViewController.currentManeuverPage else { return }