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,8 @@ class CustomViewController: UIViewController, MGLMapViewDelegate {
@IBOutlet var mapView: NavigationMapView!
@IBOutlet weak var cancelButton: UIButton!
@IBOutlet weak var instructionsBannerView: InstructionsBannerView!

var feedbackViewController: FeedbackViewController!

override func viewDidLoad() {
super.viewDidLoad()
Expand All @@ -31,6 +33,8 @@ class CustomViewController: UIViewController, MGLMapViewDelegate {

mapView.delegate = self
mapView.compassView.isHidden = true

feedbackViewController = FeedbackViewController(eventsManager: routeController.eventsManager)
Copy link
Contributor

Choose a reason for hiding this comment

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

Postpone initializing the FeedbackViewController until we present it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why is that? Is it really that expensive to just keep it in memory?

Copy link
Contributor

@frederoni frederoni Aug 20, 2018

Choose a reason for hiding this comment

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

It's not that expensive, I'd guess a few MBs but it's good practice not to allocate view controllers, views, and objects in general before use, unless it needs to be pre-cached.

In this case, the feedback view controller is not always going to be presented, e.g. if you cancel navigation or never reach the destination, making it excessive to keep in memory during the whole session.

Also, some settings or state might change after the view is loaded, e.g. landscape orientation, or accessibility settings. Then the view could've been created w/ an incorrect draggable height, font settings etc...


// Add listeners for progress updates
resumeNotifications()
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)
}
}
3 changes: 2 additions & 1 deletion MapboxNavigation/FeedbackItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ extension UIImage {
}
}

struct FeedbackItem {
@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 {
var title: String
var image: UIImage
var feedbackType: FeedbackType
Copy link
Contributor

Choose a reason for hiding this comment

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

Do any of these properties need to be public? What about the initializer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the initializer should be enough, no?

Copy link
Contributor

Choose a reason for hiding this comment

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

The initializer is now public, but the arguments passed into the initializer are thereafter inaccessible to client code, making the feedback item something of a dropbox. I think we should make these three properties public for consistency.

Expand Down
106 changes: 84 additions & 22 deletions MapboxNavigation/FeedbackViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,25 @@ import MapboxCoreNavigation
import AVFoundation

extension FeedbackViewController: UIViewControllerTransitioningDelegate {
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
Copy link
Contributor

Choose a reason for hiding this comment

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

If we make these methods public, we’ll need to give them less awkward names (forDismissing?) and documentation and consider making them accessible to Objective-C.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Noting, we have no control over the naming conventions of this method since it's a func defined on UIViewControllerTransitioningDelegate .

Copy link
Contributor

Choose a reason for hiding this comment

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

So it is! 😄 (By the way, the Objective-C method name looks fine; it’s just the Swift name that’s funky.) Oh well, carry on.

abortAutodismiss()
return DismissAnimator()
}

func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return PresentAnimator()
}

func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactor.hasStarted ? interactor : nil
}
}

typealias FeedbackSection = [FeedbackItem]

class FeedbackViewController: UIViewController, DismissDraggable, UIGestureRecognizerDelegate {

typealias SendFeedbackHandler = (FeedbackItem) -> Void
@objc(FeedbackViewController)
public class FeedbackViewController: UIViewController, DismissDraggable, UIGestureRecognizerDelegate {

var sendFeedbackHandler: SendFeedbackHandler?
var sendFeedbackHandler: ((FeedbackItem) -> Void)?
var dismissFeedbackHandler: (() -> Void)?
var sections = [FeedbackSection]()
var activeFeedbackItem: FeedbackItem?

static let sceneTitle = NSLocalizedString("FEEDBACK_TITLE", value: "Report Problem", comment: "Title of view controller for sending feedback")
Expand All @@ -35,6 +31,8 @@ class FeedbackViewController: UIViewController, DismissDraggable, UIGestureRecog

let interactor = Interactor()

public var sections: [[FeedbackItem]] = [[.turnNotAllowed, .closure, .reportTraffic, .confusingInstructions, .generalMapError, .badRoute]]
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this an array nested inside an array? What does the outer array represent?

Copy link
Contributor

Choose a reason for hiding this comment

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

Nested sections were previously used to forcefully use one or two rows based on device orientation. This is being handled automatically now:

let width = traitCollection.verticalSizeClass == .compact
? floor(availableWidth / CGFloat(items.count))
: floor(availableWidth / CGFloat(items.count / 2))

So should be fine to flatten [[FeedbackItem]] to [FeedbackItem] and always use the first section. However, keeping the current structure is a bit more versatile.


lazy var collectionView: UICollectionView = {
let view: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
view.translatesAutoresizingMaskIntoConstraints = false
Expand Down Expand Up @@ -71,7 +69,30 @@ class FeedbackViewController: UIViewController, DismissDraggable, UIGestureRecog
return fullHeight
}

override func viewDidLoad() {
var eventsManager: EventsManager
Copy link
Contributor

Choose a reason for hiding this comment

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

This property (or at least its getter) should be public. If application code can pass an EventsManger into the initializer, it should be able to see which EventsManager the object wound up with.


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()
Expand All @@ -80,14 +101,18 @@ class FeedbackViewController: UIViewController, DismissDraggable, UIGestureRecog
view.backgroundColor = .white
progressBar.barColor = #colorLiteral(red: 0.9347146749, green: 0.5047877431, blue: 0.1419634521, alpha: 1)
enableDraggableDismiss()

let defaults = defaultFeedbackHandlers() //this is done every time to refresh the feedback UUID
sendFeedbackHandler = defaults.send
dismissFeedbackHandler = defaults.dismiss
}

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)

UIView.animate(withDuration: FeedbackViewController.autoDismissInterval) {
Expand All @@ -97,7 +122,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.
Expand Down Expand Up @@ -131,7 +156,7 @@ class FeedbackViewController: UIViewController, DismissDraggable, UIGestureRecog
dismissFeedbackHandler?()
}

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,10 +190,48 @@ class FeedbackViewController: UIViewController, DismissDraggable, UIGestureRecog
progressBar.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
progressBar.bottomAnchor.constraint(equalTo: view.safeBottomAnchor).isActive = true
}

func defaultFeedbackHandlers(source: FeedbackSource = .user) -> (send: (FeedbackItem) -> Void, dismiss: () -> Void) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Inline this method into the one call site, to avoid the cognitive overhead of passing these closures around.

let uuid = eventsManager.recordFeedback()
let send = defaultSendFeedbackHandler(uuid: uuid)
let dismiss = defaultDismissFeedbackHandler(uuid: uuid)

return (send, dismiss)
}

func defaultSendFeedbackHandler(source: FeedbackSource = .user, uuid: UUID) -> (FeedbackItem) -> Void {
Copy link
Contributor

Choose a reason for hiding this comment

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

There’s only one call site and it doesn’t specify the source argument, so inline the default .user value below.

return { [weak self] (item) in
guard let strongSelf = self else { return }

// todo, can this be moved as a delegate on this view controller?
//strongSelf.delegate?.mapViewController(strongSelf, didSendFeedbackAssigned: uuid, feedbackType: item.feedbackType)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would it make sense to make a protocol for FeedbackViewController here? This would remove some plumbing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this delegate method even helpful? It only returns the uuid and the feedback type.

Copy link
Contributor

Choose a reason for hiding this comment

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

To make it useful, pass in the feedback item itself instead of the feedback type.

strongSelf.eventsManager.updateFeedback(uuid: uuid, type: item.feedbackType, source: source, description: nil)

guard let parent = strongSelf.presentingViewController else {
strongSelf.dismiss(animated: true)
return
}

strongSelf.dismiss(animated: true) {
DialogViewController().present(on: parent)
Copy link
Contributor Author

@bsudekum bsudekum Aug 14, 2018

Choose a reason for hiding this comment

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

For some reason, parent is nil here.

Copy link
Contributor

Choose a reason for hiding this comment

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

It may be because you're using a custom transitioning coordinator. If so, you may need to manually add the child VC and do all the lifecycle methods, such as UIViewController.willMove(toParentViewController:)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@JThramer solved with bc32b5e#diff-a25e8b3bfe9ceec368b83a004a6d1708R200. How do you feel about it?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah yeah. That makes a lot more sense. 👍

}
}
}

func defaultDismissFeedbackHandler(uuid: UUID) -> (() -> Void) {
return { [weak self ] in
guard let strongSelf = self else { return }

// todo, can this be moved as a delegate on this view controller?
// strongSelf.delegate?.mapViewControllerDidCancelFeedback(strongSelf)
strongSelf.eventsManager.cancelFeedback(uuid: uuid)
strongSelf.dismiss(animated: true, completion: nil)
}
}
}

extension FeedbackViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
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]

Expand All @@ -179,15 +242,15 @@ extension FeedbackViewController: UICollectionViewDataSource {
return cell
}

func numberOfSections(in collectionView: UICollectionView) -> Int {
public func numberOfSections(in collectionView: UICollectionView) -> Int {
return sections.count
}

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return sections[section].count
}

func scrollViewDidScroll(_ scrollView: UIScrollView) {
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,16 +259,15 @@ extension FeedbackViewController: UICollectionViewDataSource {
}

extension FeedbackViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
abortAutodismiss()
let item = sections[indexPath.section][indexPath.row]
sendFeedbackHandler?(item)
}
}

extension FeedbackViewController: UICollectionViewDelegateFlowLayout {

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
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.
Expand Down
50 changes: 3 additions & 47 deletions MapboxNavigation/RouteMapViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(_:))
Expand Down Expand Up @@ -115,6 +105,7 @@ class RouteMapViewController: UIViewController {
self.init()
self.routeController = routeController
self.delegate = delegate
feedbackViewController = FeedbackViewController(eventsManager: routeController.eventsManager)
automaticallyAdjustsScrollViewInsets = false
}

Expand Down Expand Up @@ -254,13 +245,7 @@ class RouteMapViewController: UIViewController {

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?) {
Expand Down Expand Up @@ -426,35 +411,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
Expand Down