diff --git a/CHANGELOG.md b/CHANGELOG.md index cc493d16ad2..809c9e75b10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * Made functions on `StatusView` public allowing for better interaction. [#1612](https://github.com/mapbox/mapbox-navigation-ios/pull/1612) * Added a `MapboxVoiceController.audioPlayer` property. You can use this property to interrupt a spoken instruction or adjust the volume. [#1596](https://github.com/mapbox/mapbox-navigation-ios/pull/1596) * Added `StyleManager.automaticallyAdjustsStyleForTimeOfDay`, `StyleManager.delegate`, and `StyleManager.styles` properties so that you can control same time-based style switching just as NavigationViewController does. [#1617](https://github.com/mapbox/mapbox-navigation-ios/pull/1617) +* `FeedbackViewController` is now public making it possible to add the feedback view to a custom navigation UI. [#1605](https://github.com/mapbox/mapbox-navigation-ios/pull/1605/). `navigationViewControllerDidOpenFeedback(_:)`, `navigationViewControllerDidCancelFeedback(_:)` and `navigationViewController(_:uuid:feedbackType)` have all been moved to `FeedbackViewControllerDelegate`. ## v0.19.1 (August 15, 2018) diff --git a/Cartfile.resolved b/Cartfile.resolved index d9d42622571..84b60e9c329 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -3,7 +3,7 @@ github "Quick/Nimble" "v7.1.3" github "Quick/Quick" "v1.3.1" github "ceeK/Solar" "2.1.0" github "mapbox/MapboxDirections.swift" "v0.22.0" -github "mapbox/mapbox-events-ios" "v0.4.3" +github "mapbox/mapbox-events-ios" "v0.4.51" github "mapbox/mapbox-voice-swift" "v0.0.1" github "mapbox/turf-swift" "v0.2.0" github "raphaelmor/Polyline" "v4.2.0" diff --git a/Examples/Swift/Base.lproj/Main.storyboard b/Examples/Swift/Base.lproj/Main.storyboard index c95a0ae6c59..9e81e4f0e9e 100644 --- a/Examples/Swift/Base.lproj/Main.storyboard +++ b/Examples/Swift/Base.lproj/Main.storyboard @@ -187,6 +187,13 @@ + @@ -196,6 +203,7 @@ + @@ -204,6 +212,7 @@ + diff --git a/Examples/Swift/CustomViewController.swift b/Examples/Swift/CustomViewController.swift index c30715fa35b..538249f62e9 100644 --- a/Examples/Swift/CustomViewController.swift +++ b/Examples/Swift/CustomViewController.swift @@ -22,6 +22,10 @@ class CustomViewController: UIViewController, MGLMapViewDelegate { @IBOutlet var mapView: NavigationMapView! @IBOutlet weak var cancelButton: UIButton! @IBOutlet weak var instructionsBannerView: InstructionsBannerView! + + lazy var feedbackViewController: FeedbackViewController = { + return FeedbackViewController(eventsManager: routeController.eventsManager) + }() override func viewDidLoad() { super.viewDidLoad() @@ -106,4 +110,8 @@ class CustomViewController: UIViewController, MGLMapViewDelegate { @IBAction func recenterMap(_ sender: Any) { mapView.recenterMap() } + + @IBAction func showFeedback(_ sender: Any) { + present(feedbackViewController, animated: true, completion: nil) + } } diff --git a/MapboxNavigation/FeedbackItem.swift b/MapboxNavigation/FeedbackItem.swift index d2806e8b36e..107b8a81d0b 100644 --- a/MapboxNavigation/FeedbackItem.swift +++ b/MapboxNavigation/FeedbackItem.swift @@ -7,12 +7,30 @@ extension UIImage { } } -struct FeedbackItem { - var title: String - var image: UIImage - var feedbackType: FeedbackType +/** + A single feedback item displayed on an instance of `FeedbackViewController`. + */ +@objc(MBFeedbackItem) +public class FeedbackItem: NSObject { + /** + The title of feedback item. This will be rendered directly below the image. + */ + @objc public var title: String - init(title: String, image: UIImage, feedbackType: FeedbackType) { + /** + An image representation of the feedback. + */ + @objc public var image: UIImage + + /** + The type of feedback that best describes the event. + */ + @objc public var feedbackType: FeedbackType + + /** + Creates a new `FeedbackItem`. + */ + @objc public init(title: String, image: UIImage, feedbackType: FeedbackType) { self.title = title self.image = image self.feedbackType = feedbackType diff --git a/MapboxNavigation/FeedbackViewController.swift b/MapboxNavigation/FeedbackViewController.swift index 1248f1d8bf0..9c74732cb84 100644 --- a/MapboxNavigation/FeedbackViewController.swift +++ b/MapboxNavigation/FeedbackViewController.swift @@ -3,29 +3,47 @@ import MapboxCoreNavigation import AVFoundation extension FeedbackViewController: UIViewControllerTransitioningDelegate { - func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + @objc public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { abortAutodismiss() return DismissAnimator() } - func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + @objc public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { return PresentAnimator() } - func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { + @objc public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return interactor.hasStarted ? interactor : nil } } -typealias FeedbackSection = [FeedbackItem] - -class FeedbackViewController: UIViewController, DismissDraggable, UIGestureRecognizerDelegate { +/** + The `FeedbackViewControllerDelegate` protocol provides methods for responding to feedback events. + */ +@objc public protocol FeedbackViewControllerDelegate { + + /** + Called when the user opens the feedback form. + */ + @objc optional func feedbackViewControllerDidOpen(_ feedbackViewController: FeedbackViewController) - typealias SendFeedbackHandler = (FeedbackItem) -> Void + /** + Called when the user submits a feedback event. + */ + @objc(feedbackViewController:didSendFeedbackItem:UUID:) + optional func feedbackViewController(_ feedbackViewController: FeedbackViewController, didSend feedbackItem: FeedbackItem, uuid: UUID) - var sendFeedbackHandler: SendFeedbackHandler? - var dismissFeedbackHandler: (() -> Void)? - var sections = [FeedbackSection]() + /** + Called when a `FeedbackViewController` is dismissed for any reason without giving explicit feedback. + */ + @objc optional func feedbackViewControllerDidCancel(_ feedbackViewController: FeedbackViewController) +} + +/** + A view controller containing a grid of buttons the user can use to denote an issue their current navigation experience. + */ +@objc(MBFeedbackViewController) +public class FeedbackViewController: UIViewController, DismissDraggable, UIGestureRecognizerDelegate { var activeFeedbackItem: FeedbackItem? static let sceneTitle = NSLocalizedString("FEEDBACK_TITLE", value: "Report Problem", comment: "Title of view controller for sending feedback") @@ -35,6 +53,13 @@ class FeedbackViewController: UIViewController, DismissDraggable, UIGestureRecog let interactor = Interactor() + /** + The feedback items that are visible and selectable by the user. + */ + public var sections: [FeedbackItem] = [.turnNotAllowed, .closure, .reportTraffic, .confusingInstructions, .generalMapError, .badRoute] + + @objc public weak var delegate: FeedbackViewControllerDelegate? + lazy var collectionView: UICollectionView = { let view: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) view.translatesAutoresizingMaskIntoConstraints = false @@ -71,25 +96,59 @@ class FeedbackViewController: UIViewController, DismissDraggable, UIGestureRecog return fullHeight } - override func viewDidLoad() { + /** + The events manager used to send feedback events. + */ + public var eventsManager: EventsManager + + var uuid: UUID { + return eventsManager.recordFeedback() + } + + /** + Initialize a new FeedbackViewController from an `EventsManager`. + */ + @objc public init(eventsManager: EventsManager) { + self.eventsManager = eventsManager + super.init(nibName: nil, bundle: nil) + commonInit() + } + + public override func encode(with aCoder: NSCoder) { + aCoder.encode(eventsManager, forKey: "EventsManager") + } + + required public init?(coder aDecoder: NSCoder) { + eventsManager = aDecoder.decodeObject(of: [EventsManager.self], forKey: "EventsManager") as? EventsManager ?? EventsManager(accessToken: nil) + super.init(coder: aDecoder) + commonInit() + } + + func commonInit() { + self.modalPresentationStyle = .custom + self.transitioningDelegate = self + } + + override public func viewDidLoad() { super.viewDidLoad() setupViews() setupConstraints() view.layoutIfNeeded() transitioningDelegate = self view.backgroundColor = .white - progressBar.barColor = #colorLiteral(red: 0.9347146749, green: 0.5047877431, blue: 0.1419634521, alpha: 1) enableDraggableDismiss() } - override func viewWillAppear(_ animated: Bool) { + override public func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) progressBar.progress = 1 } - override func viewDidAppear(_ animated: Bool) { + override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + delegate?.feedbackViewControllerDidOpen?(self) + UIView.animate(withDuration: FeedbackViewController.autoDismissInterval) { self.progressBar.progress = 0 } @@ -97,7 +156,7 @@ class FeedbackViewController: UIViewController, DismissDraggable, UIGestureRecog enableAutoDismiss() } - override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { + override public func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { super.willTransition(to: newCollection, with: coordinator) // Dismiss the feedback view when switching between landscape and portrait mode. @@ -126,12 +185,15 @@ class FeedbackViewController: UIViewController, DismissDraggable, UIGestureRecog NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(dismissFeedback), object: nil) } - @objc func dismissFeedback() { + /** + Instantly dismisses the FeedbackViewController if it is currently presented. + */ + @objc public func dismissFeedback() { abortAutodismiss() - dismissFeedbackHandler?() + dismissFeedbackItem() } - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { // Only respond to touches outside/behind the view let isDescendant = touch.view?.isDescendant(of: view) ?? true return !isDescendant @@ -165,12 +227,32 @@ class FeedbackViewController: UIViewController, DismissDraggable, UIGestureRecog progressBar.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true progressBar.bottomAnchor.constraint(equalTo: view.safeBottomAnchor).isActive = true } + + func send(_ item: FeedbackItem) { + delegate?.feedbackViewController?(self, didSend: item, uuid: uuid) + eventsManager.updateFeedback(uuid: uuid, type: item.feedbackType, source: .user, description: nil) + + guard let parent = presentingViewController else { + dismiss(animated: true) + return + } + + dismiss(animated: true) { + DialogViewController().present(on: parent) + } + } + + func dismissFeedbackItem() { + delegate?.feedbackViewControllerDidCancel?(self) + eventsManager.cancelFeedback(uuid: uuid) + dismiss(animated: true, completion: nil) + } } extension FeedbackViewController: UICollectionViewDataSource { - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + @objc public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: FeedbackCollectionViewCell.defaultIdentifier, for: indexPath) as! FeedbackCollectionViewCell - let item = sections[indexPath.section][indexPath.row] + let item = sections[indexPath.row] cell.titleLabel.text = item.title cell.imageView.tintColor = .clear @@ -179,15 +261,15 @@ extension FeedbackViewController: UICollectionViewDataSource { return cell } - func numberOfSections(in collectionView: UICollectionView) -> Int { - return sections.count + @objc public func numberOfSections(in collectionView: UICollectionView) -> Int { + return 1 } - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return sections[section].count + @objc public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return sections.count } - func scrollViewDidScroll(_ scrollView: UIScrollView) { + @objc public func scrollViewDidScroll(_ scrollView: UIScrollView) { // In case the view is scrolled, dismiss the feedback window immediately // and reset the `progressBar` back to a full progress. abortAutodismiss() @@ -196,24 +278,22 @@ extension FeedbackViewController: UICollectionViewDataSource { } extension FeedbackViewController: UICollectionViewDelegate { - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + @objc public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { abortAutodismiss() - let item = sections[indexPath.section][indexPath.row] - sendFeedbackHandler?(item) + let item = sections[indexPath.row] + send(item) } } extension FeedbackViewController: UICollectionViewDelegateFlowLayout { - - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + @objc public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let availableWidth = collectionView.bounds.width // 3 columns and 2 rows in portrait mode. // 6 columns and 1 row in landscape mode. - let items = sections[indexPath.section] let width = traitCollection.verticalSizeClass == .compact - ? floor(availableWidth / CGFloat(items.count)) - : floor(availableWidth / CGFloat(items.count / 2)) - let item = sections[indexPath.section][indexPath.row] + ? floor(availableWidth / CGFloat(sections.count)) + : floor(availableWidth / CGFloat(sections.count / 2)) + let item = sections[indexPath.row] let titleHeight = item.title.height(constrainedTo: width, font: FeedbackCollectionViewCell.Constants.titleFont) let cellHeight: CGFloat = FeedbackCollectionViewCell.Constants.imageSize.height + FeedbackCollectionViewCell.Constants.padding diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 18fba30b2cf..e03ea9e8aa1 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -150,26 +150,6 @@ public protocol NavigationViewControllerDelegate: VisualInstructionDelegate { @objc(navigationViewController:viewForAnnotation:) optional func navigationViewController(_ navigationViewController: NavigationViewController, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? - /** - Called when the user opens the feedback form. - */ - @objc optional func navigationViewControllerDidOpenFeedback(_ viewController: NavigationViewController) - - /** - Called when the user dismisses the feedback form. - */ - @objc optional func navigationViewControllerDidCancelFeedback(_ viewController: NavigationViewController) - - /** - Called when the user sends feedback. - - - parameter viewController: The navigation view controller that reported the feedback. - - parameter uuid: The feedback event’s unique identifier. - - parameter feedbackType: The type of feedback event that was sent. - */ - @objc(navigationViewController:didSendFeedbackAssignedUUID:feedbackType:) - optional func navigationViewController(_ viewController: NavigationViewController, didSendFeedbackAssigned uuid: UUID, feedbackType: FeedbackType) - /** Returns the center point of the user course view in screen coordinates relative to the map view. */ @@ -525,14 +505,6 @@ extension NavigationViewController: RouteMapViewControllerDelegate { return delegate?.navigationViewController?(self, viewFor: annotation) } - func mapViewControllerDidOpenFeedback(_ mapViewController: RouteMapViewController) { - delegate?.navigationViewControllerDidOpenFeedback?(self) - } - - func mapViewControllerDidCancelFeedback(_ mapViewController: RouteMapViewController) { - delegate?.navigationViewControllerDidCancelFeedback?(self) - } - func mapViewControllerDidDismiss(_ mapViewController: RouteMapViewController, byCanceling canceled: Bool) { if delegate?.navigationViewControllerDidDismiss?(self, byCanceling: canceled) != nil { // The receiver should handle dismissal of the NavigationViewController @@ -541,10 +513,6 @@ extension NavigationViewController: RouteMapViewControllerDelegate { } } - func mapViewController(_ mapViewController: RouteMapViewController, didSendFeedbackAssigned uuid: UUID, feedbackType: FeedbackType) { - delegate?.navigationViewController?(self, didSendFeedbackAssigned: uuid, feedbackType: feedbackType) - } - public func navigationMapViewUserAnchorPoint(_ mapView: NavigationMapView) -> CGPoint { return delegate?.navigationViewController?(self, mapViewUserAnchorPoint: mapView) ?? .zero } diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index df48d33ad7f..b59591a0745 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -26,17 +26,7 @@ class RouteMapViewController: UIViewController { return viewController }() - lazy var feedbackViewController: FeedbackViewController = { - let controller = FeedbackViewController() - - controller.sections = [ - [.turnNotAllowed, .closure, .reportTraffic, .confusingInstructions, .generalMapError, .badRoute] - ] - - controller.modalPresentationStyle = .custom - controller.transitioningDelegate = controller - return controller - }() + var feedbackViewController: FeedbackViewController! private struct Actions { static let overview: Selector = #selector(RouteMapViewController.toggleOverview(_:)) @@ -115,6 +105,7 @@ class RouteMapViewController: UIViewController { self.init() self.routeController = routeController self.delegate = delegate + feedbackViewController = FeedbackViewController(eventsManager: routeController.eventsManager) automaticallyAdjustsScrollViewInsets = false } @@ -249,18 +240,11 @@ class RouteMapViewController: UIViewController { @objc func feedback(_ sender: Any) { showFeedback() - delegate?.mapViewControllerDidOpenFeedback(self) } func showFeedback(source: FeedbackSource = .user) { guard let parent = parent else { return } - - let controller = feedbackViewController - let defaults = defaultFeedbackHandlers() //this is done every time to refresh the feedback UUID - controller.sendFeedbackHandler = defaults.send - controller.dismissFeedbackHandler = defaults.dismiss - - parent.present(controller, animated: true, completion: nil) + parent.present(feedbackViewController, animated: true, completion: nil) } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { @@ -426,35 +410,6 @@ class RouteMapViewController: UIViewController { } } - func defaultFeedbackHandlers(source: FeedbackSource = .user) -> (send: FeedbackViewController.SendFeedbackHandler, dismiss: () -> Void) { - let uuid = routeController.eventsManager.recordFeedback() - let send = defaultSendFeedbackHandler(uuid: uuid) - let dismiss = defaultDismissFeedbackHandler(uuid: uuid) - - return (send, dismiss) - } - - func defaultSendFeedbackHandler(source: FeedbackSource = .user, uuid: UUID) -> FeedbackViewController.SendFeedbackHandler { - return { [weak self] (item) in - guard let strongSelf = self, let parent = strongSelf.parent else { return } - - strongSelf.delegate?.mapViewController(strongSelf, didSendFeedbackAssigned: uuid, feedbackType: item.feedbackType) - strongSelf.routeController.eventsManager.updateFeedback(uuid: uuid, type: item.feedbackType, source: source, description: nil) - strongSelf.dismiss(animated: true) { - DialogViewController().present(on: parent) - } - } - } - - func defaultDismissFeedbackHandler(uuid: UUID) -> (() -> Void) { - return { [weak self ] in - guard let strongSelf = self else { return } - strongSelf.delegate?.mapViewControllerDidCancelFeedback(strongSelf) - strongSelf.routeController.eventsManager.cancelFeedback(uuid: uuid) - strongSelf.dismiss(animated: true, completion: nil) - } - } - var contentInsets: UIEdgeInsets { let top = navigationView.instructionsBannerContentView.bounds.height let bottom = navigationView.bottomBannerView.bounds.height @@ -1032,11 +987,7 @@ fileprivate extension UIViewAnimationOptions { } } @objc protocol RouteMapViewControllerDelegate: NavigationMapViewDelegate, MGLMapViewDelegate, VisualInstructionDelegate { - - func mapViewControllerDidOpenFeedback(_ mapViewController: RouteMapViewController) - func mapViewControllerDidCancelFeedback(_ mapViewController: RouteMapViewController) func mapViewControllerDidDismiss(_ mapViewController: RouteMapViewController, byCanceling canceled: Bool) - func mapViewController(_ mapViewController: RouteMapViewController, didSendFeedbackAssigned uuid: UUID, feedbackType: FeedbackType) func mapViewControllerShouldAnnotateSpokenInstructions(_ routeMapViewController: RouteMapViewController) -> Bool /**