Skip to content

Commit

Permalink
[ios] pull down InstructionView handle to show remaining steps (#276)
Browse files Browse the repository at this point in the history
* [iOS] Pull down cue sheet shows remaining steps

Also added some test fixture factories - am I going overboard?

* Move test factories and mark them internal

* Make expanded properties bindings

* Add TODO for discussion

* fixup! Make expanded properties bindings

* More small cleanup

---------

Co-authored-by: Ian Wagner <[email protected]>
  • Loading branch information
michaelkirk and ianthetechie authored Oct 2, 2024
1 parent 987125c commit 4320960
Show file tree
Hide file tree
Showing 16 changed files with 618 additions and 89 deletions.
8 changes: 8 additions & 0 deletions apple/Sources/FerrostarCore/NavigationState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ public struct NavigationState: Hashable {
return visualInstruction
}

public var remainingSteps: [RouteStep]? {
guard case let .navigating(_, _, remainingSteps: remainingSteps, _, _, _, _, _, _) = tripState else {
return nil
}

return remainingSteps
}

public var currentAnnotationJSON: String? {
guard case let .navigating(_, _, _, _, _, _, _, _, annotationJson: annotationJson) = tripState else {
return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ struct LandscapeNavigationOverlayView: View, CustomizableNavigatingInnerGridView

private var navigationState: NavigationState?

@State private var isInstructionViewExpanded: Bool = false

var topCenter: (() -> AnyView)?
var topTrailing: (() -> AnyView)?
var midLeading: (() -> AnyView)?
Expand Down Expand Up @@ -46,26 +48,29 @@ struct LandscapeNavigationOverlayView: View, CustomizableNavigatingInnerGridView

var body: some View {
HStack {
VStack {
ZStack(alignment: .top) {
VStack {
Spacer()
if case .navigating = navigationState?.tripState,
let progress = navigationState?.currentProgress
{
ArrivalView(
progress: progress,
onTapExit: onTapExit
)
}
}
if case .navigating = navigationState?.tripState,
let visualInstruction = navigationState?.currentVisualInstruction,
let progress = navigationState?.currentProgress
let progress = navigationState?.currentProgress,
let remainingSteps = navigationState?.remainingSteps
{
InstructionsView(
visualInstruction: visualInstruction,
distanceFormatter: formatterCollection.distanceFormatter,
distanceToNextManeuver: progress.distanceToNextManeuver
)
}

Spacer()

if case .navigating = navigationState?.tripState,
let progress = navigationState?.currentProgress
{
ArrivalView(
progress: progress,
onTapExit: onTapExit
distanceToNextManeuver: progress.distanceToNextManeuver,
remainingSteps: remainingSteps,
isExpanded: $isInstructionViewExpanded
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ struct PortraitNavigationOverlayView: View, CustomizableNavigatingInnerGridView

private var navigationState: NavigationState?

@State private var isInstructionViewExpanded: Bool = false
@State private var instructionsViewSizeWhenNotExpanded: CGSize = .zero

var topCenter: (() -> AnyView)?
var topTrailing: (() -> AnyView)?
var midLeading: (() -> AnyView)?
Expand Down Expand Up @@ -45,46 +48,54 @@ struct PortraitNavigationOverlayView: View, CustomizableNavigatingInnerGridView
}

var body: some View {
VStack {
ZStack(alignment: .top) {
VStack {
Spacer()

// The inner content is displayed vertically full screen
// when both the visualInstructions and progress are nil.
// It will automatically reduce height if and when either
// view appears
NavigatingInnerGridView(
speedLimit: speedLimit,
showZoom: showZoom,
onZoomIn: onZoomIn,
onZoomOut: onZoomOut,
showCentering: showCentering,
onCenter: onCenter
)
.innerGrid {
topCenter?()
} topTrailing: {
topTrailing?()
} midLeading: {
midLeading?()
} bottomTrailing: {
bottomTrailing?()
}

if case .navigating = navigationState?.tripState,
let progress = navigationState?.currentProgress
{
ArrivalView(
progress: progress,
onTapExit: onTapExit
)
}
}.padding(.top, instructionsViewSizeWhenNotExpanded.height)

if case .navigating = navigationState?.tripState,
let visualInstruction = navigationState?.currentVisualInstruction,
let progress = navigationState?.currentProgress
let progress = navigationState?.currentProgress,
let remainingSteps = navigationState?.remainingSteps
{
InstructionsView(
visualInstruction: visualInstruction,
distanceFormatter: formatterCollection.distanceFormatter,
distanceToNextManeuver: progress.distanceToNextManeuver
)
}

// The inner content is displayed vertically full screen
// when both the visualInstructions and progress are nil.
// It will automatically reduce height if and when either
// view appears
NavigatingInnerGridView(
speedLimit: speedLimit,
showZoom: showZoom,
onZoomIn: onZoomIn,
onZoomOut: onZoomOut,
showCentering: showCentering,
onCenter: onCenter
)
.innerGrid {
topCenter?()
} topTrailing: {
topTrailing?()
} midLeading: {
midLeading?()
} bottomTrailing: {
bottomTrailing?()
}

if case .navigating = navigationState?.tripState,
let progress = navigationState?.currentProgress
{
ArrivalView(
progress: progress,
onTapExit: onTapExit
distanceToNextManeuver: progress.distanceToNextManeuver,
remainingSteps: remainingSteps,
isExpanded: $isInstructionViewExpanded,
sizeWhenNotExpanded: $instructionsViewSizeWhenNotExpanded
)
}
}
Expand Down
95 changes: 95 additions & 0 deletions apple/Sources/FerrostarSwiftUI/TestFixtureFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/// Various helpers that generate views for previews.

import FerrostarCoreFFI
import Foundation

protocol TestFixtureFactory {
associatedtype Output
func build(_ n: Int) -> Output
}

extension TestFixtureFactory {
func buildMany(_ n: Int) -> [Output] {
(0 ... n).map { build($0) }
}
}

struct VisualInstructionContentFactory: TestFixtureFactory {
public init() {}

public var textBuilder: (Int) -> String = { n in RoadNameFactory().build(n) }
public func text(_ builder: @escaping (Int) -> String) -> Self {
var copy = self
copy.textBuilder = builder
return copy
}

public func build(_ n: Int = 0) -> VisualInstructionContent {
VisualInstructionContent(
text: textBuilder(n),
maneuverType: .turn,
maneuverModifier: .left,
roundaboutExitDegrees: nil
)
}
}

struct VisualInstructionFactory: TestFixtureFactory {
public init() {}

public var primaryContentBuilder: (Int) -> VisualInstructionContent = { n in
VisualInstructionContentFactory().build(n)
}

public var secondaryContentBuilder: (Int) -> VisualInstructionContent? = { _ in nil }

public func secondaryContent(_ builder: @escaping (Int) -> VisualInstructionContent) -> Self {
var copy = self
copy.secondaryContentBuilder = builder
return copy
}

public func build(_ n: Int = 0) -> VisualInstruction {
VisualInstruction(
primaryContent: primaryContentBuilder(n),
secondaryContent: secondaryContentBuilder(n),
triggerDistanceBeforeManeuver: 42.0
)
}
}

struct RouteStepFactory: TestFixtureFactory {
public init() {}
public var visualInstructionBuilder: (Int) -> VisualInstruction = { n in VisualInstructionFactory().build(n) }
public var roadNameBuilder: (Int) -> String = { n in RoadNameFactory().build(n) }

public func build(_ n: Int = 0) -> RouteStep {
RouteStep(
geometry: [],
distance: 100,
duration: 99,
roadName: roadNameBuilder(n),
instruction: "Walk west on \(roadNameBuilder(n))",
visualInstructions: [visualInstructionBuilder(n)],
spokenInstructions: [],
annotations: nil
)
}
}

struct RoadNameFactory: TestFixtureFactory {
public init() {}
public var baseNameBuilder: (Int) -> String = { _ in "Ave" }

public func baseName(_ builder: @escaping (Int) -> String) -> Self {
var copy = self
copy.baseNameBuilder = builder
return copy
}

public func build(_ n: Int = 0) -> String {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .ordinal
return "\(numberFormatter.string(from: NSNumber(value: n + 1))!) \(baseNameBuilder(n))"
}
}
Loading

0 comments on commit 4320960

Please sign in to comment.