-
Notifications
You must be signed in to change notification settings - Fork 26
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
Conversation
VStack { | ||
if case let .navigating(_, _, _, _, progress: progress, _, visualInstruction: visualInstruction, | ||
_) = navigationState?.tripState, | ||
ZStack(alignment: .top) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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
33f35da
to
4cee9c6
Compare
Converting to a draft, I'd like to tweak the handle styling slightly and add a snapshot test for the expanded maneuver list. |
7d67219
to
71a6ba1
Compare
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 |
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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. 👍
There was a problem hiding this 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!
apple/Sources/FerrostarMapLibreUI/Views/Overylays/LandscapeNavigationOverlayView.swift
Outdated
Show resolved
Hide resolved
VStack { | ||
if case let .navigating(_, _, _, _, progress: progress, _, visualInstruction: visualInstruction, | ||
_) = navigationState?.tripState, | ||
ZStack(alignment: .top) { |
There was a problem hiding this comment.
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 😅
Also added some test fixture factories - am I going overboard?
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.
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. |
31e7850
to
a941753
Compare
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. |
Thanks! Giving it a final check now!
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.)
All good; thanks for checking 👍 |
private var nextVisualInstructions: [(VisualInstruction, RouteStep)] { | ||
guard let remainingSteps, !remainingSteps.isEmpty else { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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
FIXES #32
cue-sheet-notched.mov.mp4
cue-sheet-non-notched.mov.mp4