Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make FeedbackViewController public #1605

Merged
merged 17 commits into from
Aug 23, 2018
2 changes: 1 addition & 1 deletion Cartfile.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 9 additions & 0 deletions Examples/Swift/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,13 @@
<constraint firstAttribute="height" constant="125" id="JIN-44-TWV"/>
</constraints>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ocq-gm-39c">
<rect key="frame" x="297" y="165" width="68" height="30"/>
<state key="normal" title="Feedback"/>
<connections>
<action selector="showFeedback:" destination="j9p-fX-jo4" eventType="touchUpInside" id="ZNw-Hv-gpO"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
Expand All @@ -196,6 +203,7 @@
<constraint firstItem="Zwg-TF-wCY" firstAttribute="top" secondItem="gqy-oH-EyZ" secondAttribute="top" id="Gay-FU-c00"/>
<constraint firstAttribute="trailing" secondItem="bFk-po-evo" secondAttribute="trailing" id="Hez-Lc-Zos"/>
<constraint firstItem="68P-Cf-VxO" firstAttribute="centerX" secondItem="KG0-bP-EJe" secondAttribute="centerX" id="LWc-VG-K7s"/>
<constraint firstItem="KG0-bP-EJe" firstAttribute="trailing" secondItem="ocq-gm-39c" secondAttribute="trailing" constant="10" id="Rlm-JZ-iLK"/>
<constraint firstAttribute="bottom" secondItem="bFk-po-evo" secondAttribute="bottom" id="a3I-5l-ygF"/>
<constraint firstItem="KG0-bP-EJe" firstAttribute="trailing" secondItem="Zeb-q8-C2a" secondAttribute="trailing" id="dSH-d3-G8i"/>
<constraint firstItem="xEg-9E-ca4" firstAttribute="leading" secondItem="KG0-bP-EJe" secondAttribute="leading" constant="8" id="gq5-Gh-Zua"/>
Expand All @@ -204,6 +212,7 @@
<constraint firstItem="Zeb-q8-C2a" firstAttribute="top" secondItem="KG0-bP-EJe" secondAttribute="top" id="nSS-iv-xNg"/>
<constraint firstAttribute="bottom" secondItem="xEg-9E-ca4" secondAttribute="bottom" constant="60" id="qmg-Mz-8ml"/>
<constraint firstItem="Zwg-TF-wCY" firstAttribute="trailing" secondItem="gqy-oH-EyZ" secondAttribute="trailing" id="tkQ-Hu-vZV"/>
<constraint firstItem="ocq-gm-39c" firstAttribute="top" secondItem="Zeb-q8-C2a" secondAttribute="bottom" constant="20" id="yJM-zZ-jVy"/>
<constraint firstItem="Zeb-q8-C2a" firstAttribute="leading" secondItem="KG0-bP-EJe" secondAttribute="leading" id="zYT-mW-YMn"/>
</constraints>
<viewLayoutGuide key="safeArea" id="KG0-bP-EJe"/>
Expand Down
8 changes: 8 additions & 0 deletions Examples/Swift/CustomViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
}
}
28 changes: 23 additions & 5 deletions MapboxNavigation/FeedbackItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class needs documentation.

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
Expand Down
147 changes: 113 additions & 34 deletions MapboxNavigation/FeedbackViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,46 @@ 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 optional func feedbackViewController(_ feedbackViewController: FeedbackViewController, didSend: FeedbackItem, UUID: UUID)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Distinguish between the argument label and argument name: didSend feedbackItem: FeedbackItem
  • The Swift name is currently feedbackViewController(_:didSend:UUID:); it should be feedbackViewController(_:didSend:uuid:)
  • The Objective-C name is currently -feedbackViewController:didSend:UUID:; it should be -feedbackViewController:didSendFeedbackItem: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 feedbackViewControllerDidCancelFeedback(_ feedbackViewController: FeedbackViewController)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to pass in the FeedbackItem so the application can distinguish between cancellations?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Negative.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then feedbackViewControllerDidCancel(_:).

}

/**
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")
Expand All @@ -35,6 +52,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
Expand Down Expand Up @@ -71,33 +95,67 @@ 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
}

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.
Expand Down Expand Up @@ -126,12 +184,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
Expand Down Expand Up @@ -165,12 +226,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?.feedbackViewControllerDidCancelFeedback?(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
Expand All @@ -179,15 +260,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()
Expand All @@ -196,24 +277,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
Expand Down
32 changes: 0 additions & 32 deletions MapboxNavigation/NavigationViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand Down
Loading