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
/**