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

[ios] pull down InstructionView handle to show remaining steps #276

Merged
merged 6 commits into from
Oct 2, 2024

Conversation

michaelkirk
Copy link
Contributor

FIXES #32

cue-sheet-notched.mov.mp4
cue-sheet-non-notched.mov.mp4

VStack {
if case let .navigating(_, _, _, _, progress: progress, _, visualInstruction: visualInstruction,
_) = navigationState?.tripState,
ZStack(alignment: .top) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The layout changes here are non-trivial.

We want to be able to expand the instruction view over top of the other content when it's expanded. But when it's not expanded, we want it to behave as if it's in a vstack with the NavigationInnerGridView.

My solution was to put the InstructionView in a ZStack above the grid, and then plumb out the InstructionView's "unexpanded height" to push down the NavigatingInnerGridView.

I struggled a bit with implementing this layout in SwiftUI. It's something I think would be natural to express in old school iOS layout constraints. Maybe there's a better way?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I'm not aware of a better way offhand either. It's one of the areas where SwiftUI and others struggle compared to springs-and-struts or auto layout.

Copy link
Contributor

Choose a reason for hiding this comment

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

There are a few conflicts unfortunately with the main branch since @Archdoog just cleaned up a bunch of stuff to clean up the pattern matching mess 😅

@@ -54,6 +54,8 @@ public struct DefaultIconographyManeuverInstructionView: View {
maneuverModifier: maneuverModifier
)
.frame(maxWidth: 48)
// REVIEW: without this, the first image in the vstack was rendering very small. Curiously subsequent items in the vstack looked reasonable.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

At the end of the day I don't think it's dangerous to include this change, but it's curious.

Here's what it looks like with this change reverted:

icon-size.mov.mp4

@michaelkirk michaelkirk force-pushed the mkirk/cue-sheet branch 2 times, most recently from 33f35da to 4cee9c6 Compare September 27, 2024 20:17
@michaelkirk michaelkirk marked this pull request as draft September 27, 2024 20:19
@michaelkirk
Copy link
Contributor Author

Converting to a draft, I'd like to tweak the handle styling slightly and add a snapshot test for the expanded maneuver list.

@michaelkirk michaelkirk force-pushed the mkirk/cue-sheet branch 5 times, most recently from 7d67219 to 71a6ba1 Compare September 28, 2024 01:01
@michaelkirk michaelkirk marked this pull request as ready for review September 28, 2024 01:09
@michaelkirk
Copy link
Contributor Author

Ok! I applied a couple last style tweaks, and updated the demo videos in the description.

/// - primaryRowTheme: The theme for the primary instruction.
/// - secondaryRowTheme: The theme for the secondary instruction.
/// - showPillControl: If true, shows a pill control (to indicate an action/expansion).
/// - isExpanded: Whether the instruction view starts expanded
/// - sizeWhenNotExpanded: Size of the InstructionsView when minimized - useful for allocating space for this view
Copy link
Contributor

Choose a reason for hiding this comment

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

I wish SwiftUI had a better model for this sort of thing. It's technically a binding, but really its sole purpose is publishing values back out to observers. Maybe we can clarify this a bit in the comment (if I'm understanding it correctly)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, you are understanding correctly — this is a Binding because we want the child component to communicate something to its parent. I think your additional documentation makes this clearer. 👍

Copy link
Contributor

@ianthetechie ianthetechie left a comment

Choose a reason for hiding this comment

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

Ok, this is really close now :) I've cleaned up a few things and left some final comments for discussion!

VStack {
if case let .navigating(_, _, _, _, progress: progress, _, visualInstruction: visualInstruction,
_) = navigationState?.tripState,
ZStack(alignment: .top) {
Copy link
Contributor

Choose a reason for hiding this comment

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

There are a few conflicts unfortunately with the main branch since @Archdoog just cleaned up a bunch of stuff to clean up the pattern matching mess 😅

@michaelkirk
Copy link
Contributor Author

I think that we can sidestep the problem by making this a binding instead of a boolean value. This way developers can still do what you intended, but with less potential confusion and more flexibility:

Funny enough, I actually had it as a binding initially, but got annoyed at not being able to fiddle with the canvas Previews, and decided to change it.

But really, exposing the "isExpanded" parameter was all just a last minute thing I added to enable snapshot tests, so I'm pretty happy to go whatever way you prefer with it.

(Side note: it's clear that you understand what's happening since your comment explains that this is only the initial value, but I think that puts you in the top like 2% of devs by SwiftUI knowledge :D)

Thank you, but this is a miscalculation on your part. 😆 I'm new to SwiftUI and any apparent competence is purely coincidental. I'm almost constantly confounded by it.

@michaelkirk
Copy link
Contributor Author

There are a few conflicts unfortunately with the main branch since @Archdoog just cleaned up a bunch of stuff to clean up the pattern matching mess 😅

I have rebased and pushed.

If this is not a rebase and force push kind of town, let me know, and I will merge with main next time.

@ianthetechie
Copy link
Contributor

Thanks! Giving it a final check now!

I'm almost constantly confounded by it.

ha! Yeah, SwiftUI is... pretty confounding. I found Thinking in SwiftUI helpful for making it SLIGHTLY less confounding... (No joke, I read the sections on state management like 25 times over while doing the initial sketches of the MapLibre SwiftUI DSL.)

If this is not a rebase and force push kind of town, let me know, and I will merge with main next time.

All good; thanks for checking 👍

@ianthetechie ianthetechie merged commit 4320960 into stadiamaps:main Oct 2, 2024
14 checks passed
private var nextVisualInstructions: [(VisualInstruction, RouteStep)] {
guard let remainingSteps, !remainingSteps.isEmpty else {
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 prefer your change because it's clearer, but I don't see how the previous version was a bug.

If remaining steps had one thing in it, then remainingSteps[1...] == [] so the result is the same, right?

Or am I missing something?

Copy link
Contributor

Choose a reason for hiding this comment

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

So I went off to code up a Swift Fiddle, because OBVIOUSLY Swift is zero-indexed, and accessing index 1 of an array of length 1 is out of bounds access...

You can subscript an array with any integer from zero up to, but not including, the count of the array. Using a negative number or an index equal to or greater than count triggers a runtime error.

But' here's a fiddle that proves your original code actually returns empty array as you claim: https://swiftfiddle.com/aw3i6znzvjcjzi3pdoefmu23sy. I've been writing Swift since the 0.x days and I have to say, this is the most surprised I've been in a very long time! 🤯

Scouring the Apple docs, I couldn't figure out why this works as it does, but your code was indeed correct 😂 If you've got any links explaining why this is the way it is, I'd be grateful to learn!

Copy link
Contributor Author

@michaelkirk michaelkirk Oct 2, 2024

Choose a reason for hiding this comment

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

let foo = ["a"]
print("foo[0] => \(foo[0])") // "a"
// print("foo[1] => \(foo[1])") // Fatal error: Index out of range

print("foo[0...] => \(foo[0...])") // ["a"]
print("foo[1...] => \(foo[1...])") // []

// And my favorite - while this works (and seems like it should):
print("foo[0...] => \(foo[0...])") // ["a"]
print("foo[0..0] => \(foo[0...0])") // ["a"]

// This blows up
print("foo[1...] => \(foo[1...])") // []
// print("foo[1..1] => \(foo[1...1])") // Fatal error: Array index is out of range

// I think this is a hint to the riddle
// print("foo[2...] => \(foo[2...])") // Swift/Range.swift:754: Fatal error: Range requires lowerBound <= upperBound

// Note the error! 
// It's obvious where the `lowerBound` is coming from (2), 
// but where is the `upperBound` coming from which is greater than 2?

func example() {
  // My guess is that accessing a RandomAccessCollection with PartialRangeFrom, 
  // converts the PartialRangeFrom to a Range (not a ClosedRange!), using the Array.length as the `upperBound` of the Range. That would explain all the behavior we're seeing.

  do {
    let rangeFrom: PartialRangeFrom = 2...
    let arrayLength = 1
    // this will panic when rangeFrom.start is greater than arrayLength - e.g. 2..<1
    let range: Range = rangeFrom.lowerBound..<arrayLength 
  }

  do {
    // but this will not panic, it'll just be empty:
    let rangeFrom: PartialRangeFrom = 1...
    let arrayLength = 1
    let range: Range = rangeFrom.lowerBound..<arrayLength 
    assert(range == 1..<1)
  }
}

Link to fiddle: https://swiftfiddle.com/l4jhe6atjjgkdnih6ot6rsyzgy

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

Successfully merging this pull request may close these issues.

[iOS] Standard view of upcoming maneuvers
2 participants