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

Fixup EndOfRouteViewController #58

Closed

Conversation

michaelkirk
Copy link
Collaborator

@michaelkirk michaelkirk commented Jun 13, 2024

DO NOT MERGE: This currently exposes a crashes due to another issue: see #57 Fixed with latest commit.

I think we'll eventually want to merge this and it might be helpful if you're trying to debug #57, which I'm currently stuck on.

Fixes #57
Fixes #62


Previously, having voice instructions was a requirement of ending the route.

Now we'll end the route when banner instructions are done and only consider voice instructions if there were any to begin with.

I also fell down the rabbit hole of fixing one bug after another once the EndOfRouteViewController (EORVC) was presented, otherwise, it's arguably better not to show it at all.

In particular, I also removed the "feedback" UI from the EORVC since the feedback mechanism was removed. See #62

@michaelkirk michaelkirk marked this pull request as draft June 13, 2024 00:50
@@ -424,19 +424,23 @@ extension RouteController: CLLocationManagerDelegate {

func updateRouteLegProgress(for location: CLLocation) {
let currentDestination = self.routeProgress.currentLeg.destination
guard let remainingVoiceInstructions = routeProgress.currentLegProgress.currentStepProgress.remainingSpokenInstructions else { return }
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This guard is what was throwing us off. We'd unconditionally return without calling didArriveAt: if not using spoken instructions.

@michaelkirk
Copy link
Collaborator Author

I've fixed the crash, though as I've documented in my commit message, I don't really understand what's going on.

I don't know very much about storyboards nor swift package manager, but my guess is that there's some problem happening between the interaction of storyboards and SPM.

Here's what I've observed:

Before this change, here's the storyboard UI for the RatingControl element:

Screenshot 2024-06-13 at 13 36 19

I see this error when presenting the EndOfRouteViewController:

[Storyboard] Unknown class _TtC40maplibre-navigation-ios_MapboxNavigation13RatingControl in Interface Builder file.

So the ViewController is successfully loaded from the bundle, but RatingControl has some prefix applied to the name, presumably that is some dynamic prefix thing that the compiler adds to namespace modules.

If I uncheck "Inherit Module from Target", the "Module" text field becomes empty:

Screenshot 2024-06-13 at 13 37 20

Re-running my route, upon completion of the route, it crashes at the same point, but with a slightly different error:

[Storyboard] Unknown class RatingControl in Interface Builder file.

Note that the RatingControl class is simple now, lacking the generated prefix from the first error.

If I then manually type "MapboxNavigation" into the "Module" field like this:

Screenshot 2024-06-13 at 13 36 09

The end of route controller now presents as expected. This is the change I'm proposing.

@michaelkirk
Copy link
Collaborator Author

michaelkirk commented Jun 14, 2024

I just keep going down the rabbit hole here. 🐇

Marking as draft again until #62 is resolved.

@michaelkirk michaelkirk force-pushed the mkirk/show-end-of-route branch 2 times, most recently from 18d451e to 7006eda Compare June 14, 2024 02:42
@michaelkirk
Copy link
Collaborator Author

I think I've arrived at something worthwhile.

Ultimately I've:

I regret that it ballooned into something as big as it did, but I think it's kind of the smallest reasonably coherent set of work. I could upstream the above changes individually, or you might find reviewing the individual commits helpful.

I did take some liberty in my implementation in that I replaced the EORVC storyboard with programatic view construction. If this is viewed as distasteful, I can rework my changes to keep the storyboard in play.

My motivation for getting rid of the storyboard was:

  1. I don't know very much about storyboards and they seem complicated and somewhat magical. For example, I really don't understand how I fixed the crash in crash when route ends: [Storyboard] Unknown class _TtC40maplibre-navigation-ios_MapboxNavigation13RatingControl in Interface Builder file. #57.

  2. It was the only storyboard view in the framework. It seems nice to reduce paradigms.

Screenshot 2024-06-13 at 19 20 50

Here's a waypoint with a pathologically long name so you can see how the layout grows:
Screenshot 2024-06-13 at 19 15 37

@@ -1,4 +1,4 @@
#!/usr/bin/env/bash
#!/usr/bin/env bash
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

typo?


# Extract localizable strings from .swift files
find ${CORE} -name "*.swift" -print0 | xargs -0 xcrun extractLocStrings -o "${CORE}/Resources/${lang}.lproj"
STRINGS_FILE="${CORE}/Resources/${lang}.lproj/Localizable.strings"

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I removed some strings, so I wanted to regenerate them. Unfortunately the script was broken, so I fixed that too.

It seems like maybe there were some other stale strings that hadn't been removed by earlier work.

@@ -70,7 +70,10 @@ extension UIView {
}

func pinInSuperview(respectingMargins margins: Bool = false) {
guard let superview else { return }
guard let superview else {
assertionFailure("superview was unexpectedly nil")
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I added this while debugging. It seems like a good check, but it's ancillary to my change. I can remove it if anyone is offended.

@@ -414,11 +416,6 @@ class RouteMapViewController: UIViewController {
self.navigationView.endOfRouteView = endOfRoute.view
self.navigationView.constrainEndOfRoute()
endOfRoute.didMove(toParent: self)

endOfRoute.dismissHandler = { [weak self] _, _ in
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Now that we're building programmatically, not from the storyboard, this moved to the initializer.

}

override func viewWillAppear(_ animated: Bool) {
super.viewWillDisappear(animated)
view.roundCorners([.topLeft, .topRight])
Copy link
Collaborator Author

@michaelkirk michaelkirk Jun 14, 2024

Choose a reason for hiding this comment

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

After converting to a programmatic view, the view was not visible unless I commented out view.roundCorners here.

view.roundCorners depends on view.bounds, which are not necessarily at their final state in viewWillAppear, though I guess they "happened to be" with the Storyboard. I've moved it to viewDidLayoutSubiews, which guarantees the views bounds are set.

Also this means we'll correctly re-compute the path if the bounds change (e.g. on device rotation).

self.stars.rating = 0
}

private func styleForUnnamedDestination() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I inlined this because it was short, and easier to understand the parallel logic of the "else" case.

For example, we now set self.staticYouHaveArrived.alpha back to 1.0 in the case that a new destination has a name after a previous one doesn't.

@michaelkirk michaelkirk force-pushed the mkirk/show-end-of-route branch 2 times, most recently from 46a289b to 25aeefc Compare June 14, 2024 03:08
case .linear:
self = .curveLinear
@unknown default:
fatalError("Unknown curve")
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There was a crash here, fixed in 2bf7799, but then since it was only used for the feedback views, I deleted it all anyway.

@michaelkirk michaelkirk marked this pull request as ready for review June 14, 2024 03:13
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillDisappear(animated)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think this was a typo?: Disappear

@michaelkirk michaelkirk changed the title Show end of route even if remainingVoiceInstructions is nil Fixup EndOfRouteViewController Jun 14, 2024
Previously, having voice instructions was a requirement of ending the
route.

Now we'll end the route when banner instructions are done *and* only
consider voice instructions if there were any to begin with.

Note: This currently crashes do to another issue with the end of route
view controller: see maplibre#57
I have very little experience with Storyboards.

Here's what I've observed:

Before this change, I see this error when presenting the EndOfRouteViewController:

    [Storyboard] Unknown class _TtC40maplibre-navigation-ios_MapboxNavigation13RatingControl in Interface Builder file.

So the ViewController is successfully loaded from the bundle, but `RatingControl` has some prefix applied to the name, presumably that is some dynamic prefix thing that the compiler adds to namespace modules.

If I disable the "Inherit Module from Target" I get:

    [Storyboard] Unknown class RatingControl in Interface Builder file.

Note the simpler class name.
@michaelkirk michaelkirk force-pushed the mkirk/show-end-of-route branch from 25aeefc to 09c8fe9 Compare June 14, 2024 23:31

self.routeProgress.currentLegProgress.userHasArrivedAtWaypoint = true
if let remainingVoiceInstructions = routeProgress.currentLegProgress.currentStepProgress.remainingSpokenInstructions {
Copy link
Collaborator

Choose a reason for hiding this comment

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

If I understand this right, if we have no voiceInstructions, then we will progress the legIndex on the second to last step, which might be very long.

To be honest I don't like the previous code here as well.

How about we rewrite this code to:

  • Check if user is on the second to last step
  • If the delegate is implemented, we ask the delegate: self.delegate?.routeController?(self, didArriveAt: currentDestination)
  • If the delegate is not implemented, I would propose something like (distance remaining to next WP < 50m or duration remaining < 5s or something like this and then advance to next leg.

Unless I am missing anything, we should also only set

        self.previousArrivalWaypoint = currentDestination
        self.routeProgress.currentLegProgress.userHasArrivedAtWaypoint = true

If the delegate returns true.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If I understand this right, if we have no voiceInstructions, then we will progress the legIndex on the second to last step, which might be very long.

Yes, that's the pre-existing logic. If you're saying it's weird, I agree with you. Maybe it was never relevant before because mapbox always included voice instructions? I honestly don't know.

If the delegate is implemented, we ask the delegate: self.delegate?.routeController?(self, didArriveAt: currentDestination)

Just in case there is confusion here, on iOS, the did in a method name connotes a hook called after something has already happened.

e.g. viewDidAppear is a hook called after the view appears, application(_:didReceiveRemoteNotification:) is a hook called after the application receives a remote notification. Contrast it with will which is called just before the thing happens.

  • viewWillAppear <- user hook
  • (view appears) <- system does this
  • viewDidAppear <- user hook

re: didArriveAt:

  • returns: True to advance to the next leg, if any, or false to remain on the completed leg.

So the existing boolean returned by didArriveAt does not answer the question "did I arrive?". It says "you have arrived. Should I continue navigating you to the next leg?".

I guess it's only relevant to multi-leg journeys. TBH I haven't used it.

I agree that the current logic for determining when you're considered to have have arrived is weird. I think your approach, to compare remaining distance/time, makes much more sense than what's there now. I will update that logic.

I don't think it's worth exposing a user over-ridable delegate method to customize this behavior. Though, I don't feel strongly about that if you do. I do feel strongly that iff we create and expose such a delegate method, it should be called something like shouldArriveAt: (as opposed to did...)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe it was never relevant before because mapbox always included voice instructions?

Yes, I think so.

So the existing boolean returned by didArriveAt does not answer the question "did I arrive?". It says "you have arrived. Should I continue navigating you to the next leg?".

Ah ok got it, yeah I still trown off by iOS conventions, thanks.

I don't think it's worth exposing a user over-ridable delegate method to customize this behavior

Ok, maybe we just make this whole function overwritable? As, to be honest we are thinking about overwriting this with some more specialized code in our app ;)

Copy link
Collaborator Author

@michaelkirk michaelkirk Jun 18, 2024

Choose a reason for hiding this comment

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

to compare remaining distance/time, makes much more sense than what's there now. I will update that logic.

I've pushed up a commit that implements this. It turns out there was already some "slop" built in. See a165167

I don't think it's worth exposing a user over-ridable delegate method to customize this behavior

Ok, maybe we just make this whole function overwritable? As, to be honest we are thinking about overwriting this with some more specialized code in our app ;)

Heh, I didn't think it was worthwhile because I was having a hard time imagining that someone would have a substantially different definition of what "arrival" means, but you have already proven me wrong! 😆 I'm happy to add a delegate method in that case.

Can you share what your definition of arrival would look like? I'm curious.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We mostly have multi leg routes, so we might want to do something different for the last waypoint compared to the others, or something like that :)

@boldtrn
Copy link
Collaborator

boldtrn commented Jun 15, 2024

Thanks a lot for looking into this @michaelkirk. I would really appreciate if you could split this into two PRs.

IMHO, the RouteController change is very critical, so I'd appreciate that in a separate PR, ideally with some tests that make sure everything is working as expected.

Since we are not using the RouteMapViewController in our app, I am 100% sure about these changes and maybe @hactar might @Patrick-Kladek would also like to review these.

@Patrick-Kladek
Copy link
Contributor

Regarding this, it would probably be a good idea to release this as 3.1 or 3.0.1 as this is needed for the old navigation. Once we release #54 this is no longer needed and would be the responsibility of the app to present such a screen.

@boldtrn
Copy link
Collaborator

boldtrn commented Jun 17, 2024

Yes makes sense, so you are fine if we don't merge #54 today, first get this fix through, release a new version and thne merge #54?

@Patrick-Kladek
Copy link
Contributor

@boldtrn yes, I don't mind if something takes a day longer to make it right. So lets try to get this done first.

@Patrick-Kladek
Copy link
Contributor

@michaelkirk any update on this?

@michaelkirk
Copy link
Collaborator Author

IMHO, the RouteController change is very critical, so I'd appreciate that in a separate PR, ideally with some tests that make sure everything is working as expected.

The RouteController changes by themselves make sure that the EORVC is presented.

But separate bugs (also fixed in this PR) will cause your app to crash when that EORVC is presented.

The result of fixing the first part without fixing the second part means more apps will crash than without any changes — that's why I coupled them in this big PR.

Am I correct that you want me to make a PR that will fix only the presentation logic, resulting in more apps crashing until further changes to the EORVC are also merged?

@boldtrn
Copy link
Collaborator

boldtrn commented Jun 18, 2024

Am I correct that you want me to make a PR that will fix only the presentation logic, resulting in more apps crashing until further changes to the EORVC are also merged?

My idea was to fix the bug in the EORVC first, as this bug seems to be concerning all users that use it?

The change in the RouteController could be independent, but doesn't have to be, I was just a bit concerned that I might block this too long as I really care about these few lines, as they are a crucial core of the navigation experience and need to work well :)

The previous logic would terminate early, at the penultimate step. I
don't understand why you would ever want this, but perhaps as an
explanation, the logic for audible instructions was reasonable. So
provided you had audio instructions, the early visual banner
instructions termination wasn't relevant.

Note, there is some slop built into this via `updateRouteStepProgress`
which advances to the next step when within the user is within `RouteControllerManeuverZoneRadius`  (40m)

```
/**
 Radius in meters the user must enter to count as completing a step. One
of two heuristics used to know when a user completes a step, see
`RouteControllerMaximumAllowedDegreeOffsetForTurnCompletion`.
 */
public var RouteControllerManeuverZoneRadius: CLLocationDistance = 40
```
@@ -425,7 +425,9 @@ extension RouteController: CLLocationManagerDelegate {
func updateRouteLegProgress(for location: CLLocation) {
let currentDestination = self.routeProgress.currentLeg.destination

guard self.routeProgress.currentLegProgress.remainingSteps.count <= 1 else { return }
guard self.routeProgress.currentLegProgress.remainingSteps.count == 0 else {
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should not do this :), can we use distance or time remaining? For this to become true, the arrival needs to be exactly triggered, which is especially an issue for multi leg routes.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, sorry, I just saw your comment. To be honest, I would prefer if we could actually create another variable like the RouteControllerManeuverZoneRadius, but for leg progress distance.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

RouteControllerManeuverZoneRadius is used 5 times in the code base, and it seems to be used pretty consistently to say:

How close do we need to be to a thing to be considered "at" that thing?

TBH, that seems exactly in line with how we're using it here. Why would we want it to be different ? Do you actually have this use case? What value would you choose for it?

Examples pulled from the code:

Are we "at" the beginning of a leg:

let isWithinDepartureStep = distanceToFirstCoordinateOnLeg < RouteControllerManeuverZoneRadius

Are we "at" the intersection:

if absoluteDistanceToIntersection <= RouteControllerManeuverZoneRadius {

Are we at/along the current step?

// If user is close to an intersection, the radius will be lower, so reroutes will fire easier.
let radius = max(reroutingTolerance, RouteControllerManeuverZoneRadius)

Are we "at" the end of a step?

if userSnapToStepDistanceFromManeuver <= RouteControllerManeuverZoneRadius,

Are we not "at" the start of the route?

lastKnownUserAbsoluteDistance > RouteControllerManeuverZoneRadius

@michaelkirk
Copy link
Collaborator Author

I'm going to close this. #54 was a really wide change, so a simple rebase is not feasible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants