Skip to content

Commit

Permalink
Merge pull request #258 from michaelkirk/mkirk/non-notched-layout
Browse files Browse the repository at this point in the history
[ios] non-notched layout
  • Loading branch information
ianthetechie authored Sep 24, 2024
2 parents 24b85b7 + 67695c5 commit f4429e3
Show file tree
Hide file tree
Showing 42 changed files with 137 additions and 35 deletions.
2 changes: 2 additions & 0 deletions apple/DemoApp/Demo/DemoNavigationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ struct DemoNavigationView: View {
}
} label: {
Text("Start Nav")
.lineLimit(1)
.minimumScaleFactor(0.5)
.font(.body.bold())
}
.disabled(routes?.isEmpty == true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public extension Route {
/// - Parameters:
/// - route: The encoded JSON data for the OSRM route.
/// - waypoints: The encoded JSON data for the OSRM waypoints.
/// - precision: The polyline precision.
/// - polylinePrecision: The polyline precision.
static func initFromOsrm(route: Data, waypoints: Data, polylinePrecision: UInt32) throws -> Route {
try createRouteFromOsrm(routeData: route, waypointData: waypoints, polylinePrecision: polylinePrecision)
}
Expand Down
2 changes: 1 addition & 1 deletion apple/Sources/FerrostarCore/FerrostarCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public protocol FerrostarCoreDelegate: AnyObject {
///
/// The usual flow is for callers to configure an instance of the core, set a ``delegate``,
/// and reuse the instance for as long as it makes sense (necessarily somewhat app-specific).
/// You can first call ``getRoutes(waypoints:userLocation:)``
/// You can first call ``getRoutes(initialLocation:waypoints:)``
/// to fetch a list of possible routes asynchronously. After selecting a suitable route (either interactively by the
/// user, or programmatically), call ``startNavigation(route:config:)`` to start a session.
///
Expand Down
2 changes: 1 addition & 1 deletion apple/Sources/FerrostarCore/URLRequestLoading.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ enum MockURLSessionError: Error {
/// Mocks network responses by URL. Super quick-and-dirty for testing with mocks.
///
/// By default, it will return an error for all requests. Register a mock by URL with
/// ``registerMock(forURL:withData:andResponse:)``
/// ``registerMock(forMethod:andURL:withData:andResponse:)``.
public class MockURLSession: URLRequestLoading {
private var urlResponseMap = [String: [URL: (Data, URLResponse)]]()

Expand Down
73 changes: 73 additions & 0 deletions apple/Sources/FerrostarMapLibreUI/Extensions/View.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import SwiftUI

extension View {
/// Given the view's `geometry`, synchronizes `childInsets`, such that
/// accumulating the child's insets with the parents insets, will be at least `minimumInset`.
///
/// ```
/// Given a minimumInset of 16:
/// +-------------------------------------------------------------+
/// | `parentGeometry` |
/// | +-----------------------------------------------------+ |
/// | | `parentGeometry.safeAreaInsets` (Top: 16) | |
/// | | +---------------------------------------------+ | |
/// | | | insets added by this method (Top: 0) | | |
/// | | | +------------------------------------+ | | |
/// | | | | | | | |
/// | | 8 | 8 | child view (self) | 16 | 0 | |
/// | | | | | | | |
/// | | | +------------------------------------+ | | |
/// | | | insets added by this method (Bottom: 0) | | |
/// | | +---------------------------------------------+ | |
/// | | `parentGeometry.safeAreaInsets` (Bottom: 20) | |
/// | +-----------------------------------------------------+ |
/// | |
/// +-------------------------------------------------------------+
/// ```
func complementSafeAreaInsets(
parentGeometry: GeometryProxy,
minimumInset: EdgeInsets = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)
) -> some View {
ComplementingSafeAreaView(content: self, parentGeometry: parentGeometry, minimumInset: minimumInset)
}

/// Do something reasonable-ish for clients that don't yet support
/// safeAreaPadding - in this case, fall back to regular padding.
func safeAreaPaddingPolyfill(_ insets: EdgeInsets) -> AnyView {
if #available(iOS 17.0, *) {
AnyView(self.safeAreaPadding(insets))
} else {
AnyView(padding(insets))
}
}
}

struct ComplementingSafeAreaView<V: View>: View {
var content: V

var parentGeometry: GeometryProxy
var minimumInset: EdgeInsets

@State
var childInsets: EdgeInsets = .init()

static func complement(parentInset: EdgeInsets, minimumInset: EdgeInsets) -> EdgeInsets {
var innerInsets = parentInset
innerInsets.top = max(0, minimumInset.top - parentInset.top)
innerInsets.bottom = max(0, minimumInset.bottom - parentInset.bottom)
innerInsets.leading = max(0, minimumInset.leading - parentInset.leading)
innerInsets.trailing = max(0, minimumInset.trailing - parentInset.trailing)
return innerInsets
}

var body: some View {
content.onAppear {
childInsets = ComplementingSafeAreaView.complement(
parentInset: parentGeometry.safeAreaInsets,
minimumInset: minimumInset
)
}.onChange(of: parentGeometry.safeAreaInsets) { newValue in
childInsets = ComplementingSafeAreaView.complement(parentInset: newValue, minimumInset: minimumInset)
}.safeAreaPaddingPolyfill(childInsets)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,32 @@ import SwiftUI
import UIKit

public enum NavigationMapViewContentInsetMode {
/// A predefined mode for landscape navigation map views
/// where the user location should appear toward the bottom of the map.
/// Dynamically determined insets suitable for landscape orientation,
/// where the user location indicator should appear toward the bottom right of the screen.
///
/// This is used to accommodate a left InstructionView
/// This mode is used to accommodate an InstructionView in a separate column, left of the content area.
///
/// - Parameter within : The `MapView`'s geometry
/// - Parameter verticalPct : How far "down" to inset the MapView overlay content. A higher number positions content
/// lower.
/// - Parameter horizontalPct : How far "right" to inset the MapView overlay content. A higher number positions
/// content farther right.
case landscape(within: GeometryProxy, verticalPct: CGFloat = 0.75, horizontalPct: CGFloat = 0.5)

/// A predefined mode for landscape navigation map views
/// where the user location should appear toward the bottom of the map.
/// Dynamically determined insets suitable for portrait orientation,
/// where the user location indicator should appear toward the bottom of the screen.
///
/// This mode is used to accommodate an InstructionView at the top of the MapView, in a single column with the
/// content area.
///
/// This is used to accommodate a top InstructionView
case portrait(within: GeometryProxy, verticalPct: CGFloat = 0.75)
/// - Parameter within : The `MapView`'s geometry
/// - Parameter verticalPct : How far "down" to inset the MapView overlay content. A higher number positions content
/// lower.
/// - Parameter minHeight : The minimum height (in points) of the content area. The content area could be larger
/// than this on sufficiently tall screens depending on `verticalPct`.
case portrait(within: GeometryProxy, verticalPct: CGFloat = 0.75, minHeight: CGFloat = 210)

/// Custom edge insets to manually control where the center of the map is.
/// Static edge insets to manually control where the center of the map is.
case edgeInset(UIEdgeInsets)

public init(orientation: UIDeviceOrientation, geometry: GeometryProxy) {
Expand All @@ -33,8 +46,10 @@ public enum NavigationMapViewContentInsetMode {
let leading = geometry.size.width * horizontalPct

return UIEdgeInsets(top: top, left: leading, bottom: 0, right: 0)
case let .portrait(geometry, verticalPct):
let top = geometry.size.height * verticalPct
case let .portrait(geometry, verticalPct, minVertical):
let ideal = geometry.size.height * verticalPct
let max = geometry.size.height - minVertical
let top = min(max, ideal)

return UIEdgeInsets(top: top, left: 0, bottom: 0, right: 0)
case let .edgeInset(uIEdgeInsets):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn

var onTapExit: (() -> Void)?

public var minimumSafeAreaInsets: EdgeInsets

/// Create a dynamically orienting navigation view. This view automatically arranges child views for both portait
/// and landscape orientations.
///
Expand All @@ -34,7 +36,8 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
/// - 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.
/// - navigationState: The current ferrostar navigation state provided by the Ferrostar core.
/// - minimumSafeAreaInsets: The minimum padding to apply from safe edges. See `complementSafeAreaInsets`.
/// - 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.
Expand All @@ -43,11 +46,13 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
camera: Binding<MapViewCamera>,
navigationCamera: MapViewCamera = .automotiveNavigation(),
navigationState: NavigationState?,
minimumSafeAreaInsets: EdgeInsets = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16),
onTapExit: (() -> Void)? = nil,
@MapViewContentBuilder makeMapContent: () -> [StyleLayerDefinition] = { [] }
) {
self.styleURL = styleURL
self.navigationState = navigationState
self.minimumSafeAreaInsets = minimumSafeAreaInsets
self.onTapExit = onTapExit

userLayers = makeMapContent()
Expand Down Expand Up @@ -94,7 +99,7 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
midLeading?()
} bottomTrailing: {
bottomTrailing?()
}
}.complementSafeAreaInsets(parentGeometry: geometry, minimumInset: minimumSafeAreaInsets)
default:
PortraitNavigationOverlayView(
navigationState: navigationState,
Expand All @@ -114,7 +119,7 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
midLeading?()
} bottomTrailing: {
bottomTrailing?()
}
}.complementSafeAreaInsets(parentGeometry: geometry, minimumInset: minimumSafeAreaInsets)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import SwiftUI

/// A landscape orientation navigation view that includes the InstructionsView and ArrivalView on the
/// leading half of the screen.
public struct LandscapeNavigationView: View {
public struct LandscapeNavigationView: View, CustomizableNavigatingInnerGridView {
@Environment(\.navigationFormatterCollection) var formatterCollection: any FormatterCollection

let styleURL: URL
Expand All @@ -25,6 +25,8 @@ public struct LandscapeNavigationView: View {

var onTapExit: (() -> Void)?

public var minimumSafeAreaInsets: EdgeInsets

/// 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.
Expand All @@ -34,7 +36,8 @@ public struct LandscapeNavigationView: View {
/// - 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.
/// - navigationState: The current ferrostar navigation state provided by the Ferrostar core.
/// - minimumSafeAreaInsets: The minimum padding to apply from safe edges. See `complementSafeAreaInsets`.
/// - 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.
Expand All @@ -43,11 +46,13 @@ public struct LandscapeNavigationView: View {
camera: Binding<MapViewCamera>,
navigationCamera: MapViewCamera = .automotiveNavigation(),
navigationState: NavigationState?,
minimumSafeAreaInsets: EdgeInsets = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16),
onTapExit: (() -> Void)? = nil,
@MapViewContentBuilder makeMapContent: () -> [StyleLayerDefinition] = { [] }
) {
self.styleURL = styleURL
self.navigationState = navigationState
self.minimumSafeAreaInsets = minimumSafeAreaInsets
self.onTapExit = onTapExit

userLayers = makeMapContent()
Expand Down Expand Up @@ -88,7 +93,7 @@ public struct LandscapeNavigationView: View {
midLeading?()
} bottomTrailing: {
bottomTrailing?()
}
}.complementSafeAreaInsets(parentGeometry: geometry, minimumInset: minimumSafeAreaInsets)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import Foundation
import SwiftUI

public extension NavigationMapView {
/// Set the MapView's content inset to a dynamically controlled navigation setting.
/// Set the MapView's content inset. See ``NavigationMapViewContentInsetMode`` for static and dynamic options.
///
/// This functionality is used to appropriate space the navigation puck/user location in the map view.
/// This functionality is used to position the navigation puck/user location in the map view
///
/// - Parameter inset: The inset mode for the navigation map view
/// - Returns: The modified NavigationMapView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ struct LandscapeNavigationOverlayView: View, CustomizableNavigatingInnerGridView
distanceFormatter: formatterCollection.distanceFormatter,
distanceToNextManeuver: progress.distanceToNextManeuver
)
.padding(.top, 16)
}

Spacer()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ struct PortraitNavigationOverlayView: View, CustomizableNavigatingInnerGridView
distanceFormatter: formatterCollection.distanceFormatter,
distanceToNextManeuver: progress.distanceToNextManeuver
)
.padding(.horizontal, 16)
}

// The inner content is displayed vertically full screen
Expand All @@ -79,14 +78,12 @@ struct PortraitNavigationOverlayView: View, CustomizableNavigatingInnerGridView
} bottomTrailing: {
bottomTrailing?()
}
.padding(.horizontal, 16)

if case let .navigating(_, _, _, _, progress: progress, _, _, _) = navigationState?.tripState {
ArrivalView(
progress: progress,
onTapExit: onTapExit
)
.padding(.horizontal, 16)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public struct PortraitNavigationView: View, CustomizableNavigatingInnerGridView
public var midLeading: (() -> AnyView)?
public var bottomTrailing: (() -> AnyView)?

public var minimumSafeAreaInsets: EdgeInsets

@Binding var camera: MapViewCamera
let navigationCamera: MapViewCamera

Expand All @@ -35,7 +37,8 @@ public struct PortraitNavigationView: View, CustomizableNavigatingInnerGridView
/// - 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.
/// - navigationState: The current ferrostar navigation state provided by the Ferrostar core.
/// - minimumSafeAreaInsets: The minimum padding to apply from safe edges. See `complementSafeAreaInsets`.
/// - 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.
Expand All @@ -44,11 +47,13 @@ public struct PortraitNavigationView: View, CustomizableNavigatingInnerGridView
camera: Binding<MapViewCamera>,
navigationCamera: MapViewCamera = .automotiveNavigation(),
navigationState: NavigationState?,
minimumSafeAreaInsets: EdgeInsets = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16),
onTapExit: (() -> Void)? = nil,
@MapViewContentBuilder makeMapContent: () -> [StyleLayerDefinition] = { [] }
) {
self.styleURL = styleURL
self.navigationState = navigationState
self.minimumSafeAreaInsets = minimumSafeAreaInsets
self.onTapExit = onTapExit

userLayers = makeMapContent()
Expand Down Expand Up @@ -90,7 +95,7 @@ public struct PortraitNavigationView: View, CustomizableNavigatingInnerGridView
midLeading?()
} bottomTrailing: {
bottomTrailing?()
}
}.complementSafeAreaInsets(parentGeometry: geometry, minimumInset: minimumSafeAreaInsets)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion apple/Sources/FerrostarSwiftUI/Views/ArrivalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public struct ArrivalView: View {
}
.padding(.leading, 32)
.padding(.trailing, 12)
.padding(.vertical, 16)
.padding(.vertical, 8)
.background(theme.backgroundColor)
.clipShape(.rect(cornerRadius: 48))
.shadow(radius: 12)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public struct NavigationUIBanner<Label: View>: View {
/// The basic Ferrostar SwiftUI button style.
///
/// - Parameters:
/// - action: The action the button performs on tap.
/// - severity: The severity of the banner.
/// - backgroundColor: The capsule's background color.
/// - label: The label subview.
public init(
Expand Down
Loading

0 comments on commit f4429e3

Please sign in to comment.