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

Feat/ios/dynamic navigation view #160

Merged
merged 11 commits into from
Jul 27, 2024
4 changes: 3 additions & 1 deletion apple/DemoApp/Demo/DemoNavigationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ struct DemoNavigationView: View {
}
}
CircleStyleLayer(identifier: "foo", source: source)
},
}
)
.innerGrid(
topCenter: {
if let errorMessage {
NavigationUIBanner(severity: .error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ public extension MapViewCamera {
return false
}

/// The default camera configured for navigation.
/// The default camera for automotive navigation.
///
/// - Parameters:
/// - zoom: The zoom value (default is 18.0)
/// - pitch: The pitch (default is 45.0)
/// - Returns: The configured MapViewCamera
static func navigation(zoom: Double = 18.0, pitch: Double = 45.0) -> MapViewCamera {
static func automotiveNavigation(zoom: Double = 18.0, pitch: Double = 45.0) -> MapViewCamera {
MapViewCamera.trackUserLocationWithCourse(zoom: zoom,
pitch: pitch)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ public enum NavigationMapViewContentInsetMode {
/// Custom edge insets to manually control where the center of the map is.
case edgeInset(UIEdgeInsets)

public init(orientation: UIDeviceOrientation, geometry: GeometryProxy) {
switch orientation {
case .landscapeLeft, .landscapeRight:
self = .landscape(within: geometry)
default:
self = .portrait(within: geometry)
}
}

var uiEdgeInsets: UIEdgeInsets {
switch self {
case let .landscape(geometry, verticalPct, horizontalPct):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,7 @@ import MapLibreSwiftUI
import SwiftUI

/// A navigation view that dynamically switches between portrait and landscape orientations.
public struct DynamicallyOrientingNavigationView<
TopCenter: View,
TopTrailing: View,
MidLeading: View,
BottomTrailing: View
>: View {
public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingInnerGridView {
@Environment(\.navigationFormatterCollection) var formatterCollection: any FormatterCollection

@State private var orientation = UIDevice.current.orientation
Expand All @@ -24,78 +19,103 @@ public struct DynamicallyOrientingNavigationView<
private var navigationState: NavigationState?
private let userLayers: [StyleLayerDefinition]

var topCenter: TopCenter
var topTrailing: TopTrailing
var midLeading: MidLeading
var bottomTrailing: BottomTrailing
public var topCenter: (() -> AnyView)?
public var topTrailing: (() -> AnyView)?
public var midLeading: (() -> AnyView)?
public var bottomTrailing: (() -> AnyView)?
Comment on lines +22 to +25
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to add these to the constructor? Or I guess there's an extension so maybe the idea is that you don't need to override(?) these very often so it's easier to just leave it to extension methods?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nope. To keep the constructors sane, I think these only get modified exclusively through the extension view modifier style.


var onTapExit: (() -> Void)?

/// Initialize a map view tuned for turn by turn navigation.
/// Create a dynamically orienting navigation view. This view automatically arranges child views for both portait
/// and landscape orientations.
///
/// - Parameters:
/// - styleURL: The style URL for the map. This can dynamically change between light and dark mode.
/// - navigationState: The ferrostar navigations state. This is used primarily to drive user location on the map.
/// - camera: The camera which is controlled by the navigation state, but may also be pushed to for other cases
/// (e.g. user pan).
/// - snappedZoom: The zoom for the snapped camera. This can be fixed, customized or controlled by the camera.
/// - useSnappedCamera: Whether to use the ferrostar snapped camera or the camer binding itself.
/// - distanceFormatter: The formatter for distances in instruction views.
/// - styleURL: The map's style url.
/// - camera: The camera binding that represents the current camera on the map.
/// - navigationCamera: The default navigation camera. This sets the initial camera & is also used when the center
/// on user button it tapped.
/// - navigationState: The current ferrostar navigation state provided by ferrostar core.
/// - onTapExit: An optional behavior to run when the ArrivalView exit button is tapped. When nil (default) the
/// exit button is hidden.
/// - makeMapContent: Custom maplibre symbols to display on the map view.
public init(
styleURL: URL,
camera: Binding<MapViewCamera>,
navigationCamera: MapViewCamera = .navigation(),
navigationCamera: MapViewCamera = .automotiveNavigation(),
navigationState: NavigationState?,
onTapExit: (() -> Void)? = nil,
@MapViewContentBuilder makeMapContent: () -> [StyleLayerDefinition] = { [] },
@ViewBuilder topCenter: () -> TopCenter = { Spacer() },
@ViewBuilder topTrailing: () -> TopTrailing = { Spacer() },
@ViewBuilder midLeading: () -> MidLeading = { Spacer() },
@ViewBuilder bottomTrailing: () -> BottomTrailing = { Spacer() }
@MapViewContentBuilder makeMapContent: () -> [StyleLayerDefinition] = { [] }
) {
self.styleURL = styleURL
self.navigationState = navigationState
self.onTapExit = onTapExit

userLayers = makeMapContent()
self.topCenter = topCenter()
self.topTrailing = topTrailing()
self.midLeading = midLeading()
self.bottomTrailing = bottomTrailing()

_camera = camera
self.navigationCamera = navigationCamera
}

public var body: some View {
Group {
switch orientation {
case .landscapeLeft, .landscapeRight:
LandscapeNavigationView(
GeometryReader { geometry in
ZStack {
NavigationMapView(
styleURL: styleURL,
camera: $camera,
navigationCamera: navigationCamera,
navigationState: navigationState,
onTapExit: onTapExit,
makeMapContent: { userLayers },
topCenter: { topCenter },
topTrailing: { topTrailing },
midLeading: { midLeading },
bottomTrailing: { bottomTrailing }
)
default:
PortraitNavigationView(
styleURL: styleURL,
camera: $camera,
navigationCamera: navigationCamera,
navigationState: navigationState,
onTapExit: onTapExit,
makeMapContent: { userLayers },
topCenter: { topCenter },
topTrailing: { topTrailing },
midLeading: { midLeading },
bottomTrailing: { bottomTrailing }
)
onStyleLoaded: { _ in
camera = navigationCamera
}
) {
userLayers
}
.navigationMapViewContentInset(NavigationMapViewContentInsetMode(
orientation: orientation,
geometry: geometry
))

switch orientation {
case .landscapeLeft, .landscapeRight:
LandscapeNavigationOverlayView(
navigationState: navigationState,
speedLimit: nil,
showZoom: true,
onZoomIn: { camera.incrementZoom(by: 1) },
onZoomOut: { camera.incrementZoom(by: -1) },
showCentering: !camera.isTrackingUserLocationWithCourse,
onCenter: { camera = navigationCamera },
onTapExit: onTapExit
)
.innerGrid {
topCenter?()
} topTrailing: {
topTrailing?()
} midLeading: {
midLeading?()
} bottomTrailing: {
bottomTrailing?()
}
default:
PortraitNavigationOverlayView(
navigationState: navigationState,
speedLimit: nil,
showZoom: true,
onZoomIn: { camera.incrementZoom(by: 1) },
onZoomOut: { camera.incrementZoom(by: -1) },
showCentering: !camera.isTrackingUserLocationWithCourse,
onCenter: { camera = navigationCamera },
onTapExit: onTapExit
)
.innerGrid {
topCenter?()
} topTrailing: {
topTrailing?()
} midLeading: {
midLeading?()
} bottomTrailing: {
bottomTrailing?()
}
}
}
}
.onReceive(
Expand Down
103 changes: 41 additions & 62 deletions apple/Sources/FerrostarMapLibreUI/Views/LandscapeNavigationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import MapLibreSwiftDSL
import MapLibreSwiftUI
import SwiftUI

/// A portrait orientation navigation view that includes the InstructionsView at the top.
public struct LandscapeNavigationView<TopCenter: View, TopTrailing: View, MidLeading: View,
BottomTrailing: View>: View
{
/// A landscape orientation navigation view that includes the InstructionsView and ArrivalView on the
/// leading half of the screen.
public struct LandscapeNavigationView: View {
@Environment(\.navigationFormatterCollection) var formatterCollection: any FormatterCollection

let styleURL: URL
Expand All @@ -19,35 +18,39 @@ public struct LandscapeNavigationView<TopCenter: View, TopTrailing: View, MidLea
private var navigationState: NavigationState?
private let userLayers: [StyleLayerDefinition]

var topCenter: TopCenter
var topTrailing: TopTrailing
var midLeading: MidLeading
var bottomTrailing: BottomTrailing
public var topCenter: (() -> AnyView)?
public var topTrailing: (() -> AnyView)?
public var midLeading: (() -> AnyView)?
public var bottomTrailing: (() -> AnyView)?

var onTapExit: (() -> Void)?

/// Create a landscape navigation view. This view is optimized for display on a landscape screen where the
/// instructions are on the leading half of the screen
/// and the user puck and route are on the trailing half of the screen.
///
/// - Parameters:
/// - styleURL: The map's style url.
/// - camera: The camera binding that represents the current camera on the map.
/// - navigationCamera: The default navigation camera. This sets the initial camera & is also used when the center
/// on user button it tapped.
/// - navigationState: The current ferrostar navigation state provided by ferrostar core.
/// - onTapExit: An optional behavior to run when the ArrivalView exit button is tapped. When nil (default) the
/// exit button is hidden.
/// - makeMapContent: Custom maplibre symbols to display on the map view.
public init(
styleURL: URL,
camera: Binding<MapViewCamera>,
navigationCamera: MapViewCamera = .navigation(),
navigationCamera: MapViewCamera = .automotiveNavigation(),
navigationState: NavigationState?,
onTapExit: (() -> Void)? = nil,
@MapViewContentBuilder makeMapContent: () -> [StyleLayerDefinition] = { [] },
@ViewBuilder topCenter: () -> TopCenter = { Spacer() },
@ViewBuilder topTrailing: () -> TopTrailing = { Spacer() },
@ViewBuilder midLeading: () -> MidLeading = { Spacer() },
@ViewBuilder bottomTrailing: () -> BottomTrailing = { Spacer() }
@MapViewContentBuilder makeMapContent: () -> [StyleLayerDefinition] = { [] }
) {
self.styleURL = styleURL
self.navigationState = navigationState
self.onTapExit = onTapExit

userLayers = makeMapContent()
self.topCenter = topCenter()
self.topTrailing = topTrailing()
self.midLeading = midLeading()
self.bottomTrailing = bottomTrailing()

_camera = camera
self.navigationCamera = navigationCamera
}
Expand All @@ -60,55 +63,31 @@ public struct LandscapeNavigationView<TopCenter: View, TopTrailing: View, MidLea
camera: $camera,
navigationState: navigationState,
onStyleLoaded: { _ in
camera = .navigation()
camera = navigationCamera
}
) {
userLayers
}
.navigationMapViewContentInset(.landscape(within: geometry))

HStack {
VStack {
if let navigationState,
let visualInstructions = navigationState.visualInstruction
{
InstructionsView(
visualInstruction: visualInstructions,
distanceFormatter: formatterCollection.distanceFormatter,
distanceToNextManeuver: navigationState.progress?.distanceToNextManeuver
)
.padding(.top, 16)
}

Spacer()

if let progress = navigationState?.progress {
ArrivalView(
progress: progress,
onTapExit: onTapExit
)
}
}

Spacer().frame(width: 16)

// 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
// TODO: Add dynamic speed, zoom & centering.
NavigatingInnerGridView(
speedLimit: nil,
showZoom: true,
onZoomIn: { camera.incrementZoom(by: 1) },
onZoomOut: { camera.incrementZoom(by: -1) },
showCentering: !camera.isTrackingUserLocationWithCourse,
onCenter: { camera = navigationCamera },
topCenter: { topCenter },
topTrailing: { topTrailing },
midLeading: { midLeading },
bottomTrailing: { bottomTrailing }
)
LandscapeNavigationOverlayView(
navigationState: navigationState,
speedLimit: nil,
showZoom: true,
onZoomIn: { camera.incrementZoom(by: 1) },
onZoomOut: { camera.incrementZoom(by: -1) },
showCentering: !camera.isTrackingUserLocationWithCourse,
onCenter: { camera = navigationCamera },
onTapExit: onTapExit
)
.innerGrid {
topCenter?()
} topTrailing: {
topTrailing?()
} midLeading: {
midLeading?()
} bottomTrailing: {
bottomTrailing?()
}
}
}
Expand Down
14 changes: 5 additions & 9 deletions apple/Sources/FerrostarMapLibreUI/Views/NavigationMapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,11 @@ public struct NavigationMapView: View {
/// Initialize a map view tuned for turn by turn navigation.
///
/// - Parameters:
/// - styleURL: The style URL for the map. This can dynamically change between light and dark mode.
/// - navigationState: The ferrostar navigations state. This is used primarily to drive user location on the map.
/// - camera: The camera which is controlled by the navigation state, but may also be pushed to for other cases
/// (e.g. user pan).
/// - snappedZoom: The zoom for the snapped camera. This can be fixed, customized or controlled by the camera.
/// - useSnappedCamera: Whether to use the ferrostar snapped camera or the camer binding itself.
/// - snappingBreakawayVelocity: The drag gesture velocity used to disable snapping. This can be tuned to prevent
/// accidental drags.
/// - content: Any additional MapLibre symbols to show on the map.
/// - styleURL: The map's style url.
/// - camera: The camera binding that represents the current camera on the map.
/// - navigationState: The current ferrostar navigation state provided by ferrostar core.
/// - onStyleLoaded: The map's style has loaded and the camera can be manipulated (e.g. to user tracking).
/// - makeMapContent: Custom maplibre symbols to display on the map view.
public init(
styleURL: URL,
camera: Binding<MapViewCamera>,
Expand Down
Loading
Loading