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
Merged

Make FeedbackViewController public #1605

merged 17 commits into from
Aug 23, 2018

Conversation

bsudekum
Copy link
Contributor

@bsudekum bsudekum commented Aug 14, 2018

This PR makes the FeedbackViewController public for developers to show within their custom navigation UI.

todo:

  • document.
  • convert public structs to classes.
  • figure out why self.parent is nil in many instances.

/cc @mapbox/navigation-ios

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.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. 👍

Bobby Sudekum added 2 commits August 14, 2018 15:02
@@ -7,7 +7,7 @@ extension UIImage {
}
}

struct FeedbackItem {
public struct FeedbackItem {
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that a Swift struct can’t bridge to Objective-C, nor can any method or property that uses that type.

@@ -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.

override func viewDidLoad() {
var eventsManager: EventsManager

public init(for 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.

Use a more descriptive argument label: init(eventsManager:).

}

required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this method be implemented?

@@ -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...

@bsudekum
Copy link
Contributor Author

This is ready for review.

@@ -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.

@@ -7,7 +7,8 @@ extension UIImage {
}
}

struct FeedbackItem {
@objc(MBFeedbackItem)
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.

/**
The feedback items that are visible and selectable by the user.
*/
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.

@@ -71,7 +75,33 @@ 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.

@@ -165,10 +202,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.

@bsudekum
Copy link
Contributor Author

@1ec5 updated

Copy link
Contributor

@1ec5 1ec5 left a comment

Choose a reason for hiding this comment

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

To-do items need to be addressed or removed before merging.

@@ -7,7 +7,8 @@ extension UIImage {
}
}

struct FeedbackItem {
@objc(MBFeedbackItem)
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.

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.

/**
Creates a new `FeedbackItem`.
*/
public init(title: String, image: UIImage, 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.

Add @objc to make this initializer accessible to Objective-C code.

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

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.

@@ -165,12 +205,42 @@ class FeedbackViewController: UIViewController, DismissDraggable, UIGestureRecog
progressBar.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
progressBar.bottomAnchor.constraint(equalTo: view.safeBottomAnchor).isActive = true
}

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.


let uuid = eventsManager.recordFeedback()
sendFeedbackHandler = defaultSendFeedbackHandler(uuid: uuid)
dismissFeedbackHandler = defaultDismissFeedbackHandler(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.

Instead of currying closures within defaultSendFeedbackHandler(uuid:) and defaultDismissFeedbackHandler(uuid:), pull those methods into simple send(_:withUUID:) and dismiss(_:) methods and store uuid on the class instead. Then, instead of calling sendFeedbackHandler, call send(_:withUUID:).

@@ -39,6 +57,8 @@ public class FeedbackViewController: UIViewController, DismissDraggable, UIGestu
*/
public var sections: [FeedbackItem] = [.turnNotAllowed, .closure, .reportTraffic, .confusingInstructions, .generalMapError, .badRoute]

public var delegate: FeedbackViewControllerDelegate?
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: weak

@@ -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 sendFeedback(_ item: FeedbackItem) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: send(_:) is the more Swiftian way of naming it these days, or sendFeedbackItem(_:) if there’s a conflict with another send(_:).

}
}

func defaultDismissFeedback() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Just dismissFeedbackItem(), no longer “default”.

/**
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(_:).

/**
Called when the user submits a feedback event.
*/
@objc optional func feedbackViewController(_ feedbackViewController: FeedbackViewController, didSendFeedbackAssigned: UUID, feedback: FeedbackItem)
Copy link
Contributor

Choose a reason for hiding this comment

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

Name this method feedbackViewController(_:didSend:uuid:) in Swift and -feedbackViewController:didSendFeedbackItem:UUID: in Objective-C.

/**
Called when the user opens the feedback form.
*/
@objc optional func feedbackViewControllerDidOpenFeedback(_ 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.

Name this method feedbackViewControllerDidOpen(_:), since it gets called when opening the form, not when opening a specific piece of feedback.

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

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

Then feedbackViewControllerDidCancel(_:).

Copy link
Contributor

@1ec5 1ec5 left a comment

Choose a reason for hiding this comment

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

Remember to mention any new APIs in the changelog.

@bsudekum bsudekum merged commit ae4dfb5 into master Aug 23, 2018
@bsudekum bsudekum deleted the public-feedback branch August 23, 2018 21:25
@1ec5 1ec5 added the backwards incompatible changes that break backwards compatibility of public API label Dec 18, 2018
@1ec5 1ec5 added this to the v0.20.0 milestone Dec 18, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backwards incompatible changes that break backwards compatibility of public API
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants