diff --git a/MapboxNavigation/EndOfRouteViewController.swift b/MapboxNavigation/EndOfRouteViewController.swift index ab30a551..e276f7ee 100644 --- a/MapboxNavigation/EndOfRouteViewController.swift +++ b/MapboxNavigation/EndOfRouteViewController.swift @@ -66,9 +66,9 @@ class EndOfRouteViewController: UIViewController { lazy var endOfRouteArrivedText: String = NSLocalizedString("END_OF_ROUTE_ARRIVED", bundle: .mapboxNavigation, value: "You have arrived", comment: "Title used for arrival") lazy var endNavigationText: String = NSLocalizedString("END_NAVIGATION", bundle: .mapboxNavigation, value: "End Navigation", comment: "End Navigation Button Text") - let dismissHandler: () -> Void - init(dismissHandler: @escaping () -> Void) { - self.dismissHandler = dismissHandler + var dismissHandler: (() -> Void)? + + init() { super.init(nibName: nil, bundle: nil) } @@ -107,7 +107,8 @@ class EndOfRouteViewController: UIViewController { // MARK: - Actions @objc func endNavigationPressed(_ sender: Any) { - self.dismissHandler() + assert(self.dismissHandler != nil) + self.dismissHandler?() } // MARK: - Private Functions diff --git a/MapboxNavigation/NavigationView.swift b/MapboxNavigation/NavigationView.swift index 8453c117..310ab90e 100644 --- a/MapboxNavigation/NavigationView.swift +++ b/MapboxNavigation/NavigationView.swift @@ -58,7 +58,13 @@ open class NavigationView: UIView { self.informationStackView.bottomAnchor.constraint(equalTo: self.topAnchor), self.instructionsBannerContentView.topAnchor.constraint(equalTo: self.instructionsBannerView.topAnchor) ] - + + lazy var endOfRouteShowConstraint: NSLayoutConstraint? = self.endOfRouteView?.bottomAnchor.constraint(equalTo: self.bottomAnchor) + + lazy var endOfRouteHideConstraint: NSLayoutConstraint? = self.endOfRouteView?.topAnchor.constraint(equalTo: self.bottomAnchor) + + lazy var endOfRouteHeightConstraint: NSLayoutConstraint? = self.endOfRouteView?.heightAnchor.constraint(equalToConstant: Constants.endOfRouteHeight) + private enum Images { static let overview = UIImage(named: "overview", in: .mapboxNavigation, compatibleWith: nil)!.withRenderingMode(.alwaysTemplate) static let volumeUp = UIImage(named: "volume_up", in: .mapboxNavigation, compatibleWith: nil)!.withRenderingMode(.alwaysTemplate) @@ -123,7 +129,21 @@ open class NavigationView: UIView { view.cancelButton.addTarget(self, action: Actions.cancelButton, for: .touchUpInside) return view }() - + + var endOfRouteView: UIView? { + didSet { + if let active: [NSLayoutConstraint] = constraints(affecting: oldValue) { + NSLayoutConstraint.deactivate(active) + } + + oldValue?.removeFromSuperview() + if let endOfRouteView { + endOfRouteView.translatesAutoresizingMaskIntoConstraints = false + addSubview(endOfRouteView) + } + } + } + weak var delegate: NavigationViewDelegate? { didSet { self.updateDelegates() diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index a17fde98..f91423ca 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -258,7 +258,12 @@ open class NavigationViewController: UIViewController { An instance of `MLNAnnotation` representing the origin of your route. */ public var origin: MLNAnnotation? - + + /** + Shows End of route Feedback UI when the route controller arrives at the final destination. Defaults to `true.` + */ + public var showsEndOfRouteFeedback: Bool = true + /** The receiver’s delegate. */ @@ -640,8 +645,12 @@ extension NavigationViewController: RouteControllerDelegate { if !self.isConnectedToCarPlay, // CarPlayManager shows rating on CarPlay if it's connected routeController.routeProgress.isFinalLeg, advancesToNextLeg { - self.mapViewController.transitionToEndNavigation(with: 1) - self.delegate?.navigationViewControllerDidFinishRouting?(self) + self.mapViewController.completeRoute(showArrivalUI: self.showsEndOfRouteFeedback, onDismiss: { [weak self] in + guard let self else { + return + } + self.delegate?.navigationViewControllerDidFinishRouting?(self) + }) } return advancesToNextLeg } diff --git a/MapboxNavigation/NavigationViewLayout.swift b/MapboxNavigation/NavigationViewLayout.swift index 456c4cad..2f05bae5 100644 --- a/MapboxNavigation/NavigationViewLayout.swift +++ b/MapboxNavigation/NavigationViewLayout.swift @@ -40,4 +40,13 @@ extension NavigationView { ]) NSLayoutConstraint.activate(self.bannerShowConstraints) } + + func constrainEndOfRoute() { + endOfRouteHideConstraint?.isActive = true + + endOfRouteView?.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true + endOfRouteView?.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true + + endOfRouteHeightConstraint?.isActive = true + } } diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index 37d8efed..f8c3e975 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -51,7 +51,6 @@ class RouteMapViewController: UIViewController { var lastTimeUserRerouted: Date? var stepsViewController: StepsViewController? var destination: Waypoint? - var showsEndOfRoute: Bool = true var arrowCurrentStep: RouteStep? var contentInsets: UIEdgeInsets { @@ -124,18 +123,7 @@ class RouteMapViewController: UIViewController { right: 20) } - lazy var endOfRouteViewController: EndOfRouteViewController = .init(dismissHandler: { [weak self] in - guard let self else { - return - } - guard let routeController else { - assertionFailure("routeController was unexpectedly nil") - return - } - routeController.endNavigation() - self.delegate?.mapViewControllerDidFinish(self, byCanceling: false) - }) - + let endOfRouteViewController = EndOfRouteViewController() weak var delegate: RouteMapViewControllerDelegate? // MARK: - Lifecycle @@ -454,8 +442,60 @@ class RouteMapViewController: UIViewController { } // MARK: End Of Route - - func transitionToEndNavigation(with duration: TimeInterval, completion: ((Bool) -> Void)? = nil) { + + func completeRoute(showArrivalUI: Bool, presentationDuration: TimeInterval = 1.0, presentationCompletion: ((Bool) -> Void)? = nil, onDismiss: @escaping () -> Void) { + if showArrivalUI { + self.showEndOfRoute(duration: presentationDuration, onDismiss: onDismiss, presentationCompletion: presentationCompletion) + } else { + self.transitionToEndNavigation(duration: presentationDuration, completion: { completed in + onDismiss() + presentationCompletion?(completed) + }) + } + } + + func embedEndOfRoute() { + let endOfRoute = self.endOfRouteViewController + addChild(endOfRoute) + self.navigationView.endOfRouteView = endOfRoute.view + self.navigationView.constrainEndOfRoute() + endOfRoute.didMove(toParent: self) + } + + func unembedEndOfRoute() { + let endOfRoute = self.endOfRouteViewController + endOfRoute.willMove(toParent: nil) + endOfRoute.removeFromParent() + } + + func showEndOfRoute(duration: TimeInterval = 1.0, onDismiss: @escaping () -> Void, presentationCompletion: ((Bool) -> Void)? = nil) { + self.embedEndOfRoute() + self.endOfRouteViewController.dismissHandler = onDismiss + self.endOfRouteViewController.destination = self.destination + self.navigationView.endOfRouteView?.isHidden = false + + self.view.layoutIfNeeded() // flush layout queue + self.navigationView.endOfRouteHideConstraint?.isActive = false + self.navigationView.endOfRouteShowConstraint?.isActive = true + + self.transitionToEndNavigation(duration: duration, completion: presentationCompletion) + + guard let height = navigationView.endOfRouteHeightConstraint?.constant else { return } + let insets = UIEdgeInsets(top: navigationView.instructionsBannerView.bounds.height, left: 20, bottom: height + 20, right: 20) + + // zoom in a bit to focus on the arrived destination + if let coordinates = routeController?.routeProgress.route.coordinates, let userLocation = routeController?.locationManager.location?.coordinate { + let slicedLine = Polyline(coordinates).sliced(from: userLocation).coordinates + let line = MLNPolyline(coordinates: slicedLine, count: UInt(slicedLine.count)) + + let camera = self.navigationView.mapView.cameraThatFitsShape(line, direction: self.navigationView.mapView.camera.heading, edgePadding: insets) + camera.pitch = 0 + camera.altitude = self.navigationView.mapView.camera.altitude + self.navigationView.mapView.setCamera(camera, animated: true) + } + } + + func transitionToEndNavigation(duration: TimeInterval = 1.0, completion: ((Bool) -> Void)? = nil) { self.view.layoutIfNeeded() // flush layout queue NSLayoutConstraint.deactivate(self.navigationView.bannerShowConstraints) NSLayoutConstraint.activate(self.navigationView.bannerHideConstraints) @@ -478,6 +518,8 @@ class RouteMapViewController: UIViewController { func hideEndOfRoute(duration: TimeInterval = 0.3, completion: ((Bool) -> Void)? = nil) { self.view.layoutIfNeeded() // flush layout queue + self.navigationView.endOfRouteHideConstraint?.isActive = true + self.navigationView.endOfRouteShowConstraint?.isActive = false self.view.clipsToBounds = true self.mapView.enableFrameByFrameCourseViewTracking(for: duration) @@ -488,14 +530,19 @@ class RouteMapViewController: UIViewController { self.navigationView.floatingStackView.alpha = 1.0 } + let complete: (Bool) -> Void = { + self.navigationView.endOfRouteView?.isHidden = true + self.unembedEndOfRoute() + completion?($0) + } + let noAnimation = { animate() - completion?(true) + complete(true) } guard duration > 0.0 else { return noAnimation() } - - UIView.animate(withDuration: duration, delay: 0.0, options: [.curveLinear], animations: animate, completion: completion) + UIView.animate(withDuration: duration, delay: 0.0, options: [.curveLinear], animations: animate, completion: complete) } }