diff --git a/apple/Sources/FerrostarCarPlayUI/Views/CarPlayNavigationView.swift b/apple/Sources/FerrostarCarPlayUI/Views/CarPlayNavigationView.swift index 8846eb45f..f22a45891 100644 --- a/apple/Sources/FerrostarCarPlayUI/Views/CarPlayNavigationView.swift +++ b/apple/Sources/FerrostarCarPlayUI/Views/CarPlayNavigationView.swift @@ -16,6 +16,7 @@ public struct CarPlayNavigationView: View { let styleURL: URL @Binding var camera: MapViewCamera let navigationCamera: MapViewCamera + let locationManagerConfiguration: NavigationLocationManagerConfiguration? private let navigationState: NavigationState? private let userLayers: [StyleLayerDefinition] @@ -42,11 +43,13 @@ public struct CarPlayNavigationView: View { camera: Binding, navigationCamera: MapViewCamera = .automotiveNavigation(), navigationState: NavigationState?, + locationManagerConfiguration: NavigationLocationManagerConfiguration? = nil, minimumSafeAreaInsets: EdgeInsets = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16), @MapViewContentBuilder makeMapContent: () -> [StyleLayerDefinition] = { [] } ) { self.styleURL = styleURL self.navigationState = navigationState + self.locationManagerConfiguration = locationManagerConfiguration self.minimumSafeAreaInsets = minimumSafeAreaInsets userLayers = makeMapContent() @@ -61,6 +64,7 @@ public struct CarPlayNavigationView: View { styleURL: styleURL, camera: $camera, navigationState: navigationState, + locationManagerConfiguration: locationManagerConfiguration, onStyleLoaded: { _ in camera = navigationCamera } diff --git a/apple/Sources/FerrostarMapLibreUI/Models/NavigationDrivenLocationManager.swift b/apple/Sources/FerrostarMapLibreUI/Models/NavigationDrivenLocationManager.swift new file mode 100644 index 000000000..e270ef5e7 --- /dev/null +++ b/apple/Sources/FerrostarMapLibreUI/Models/NavigationDrivenLocationManager.swift @@ -0,0 +1,11 @@ +import CoreLocation +import MapLibre +import MapLibreSwiftUI + +/// A map location manager that can be directly fed by navigation state updates. +public protocol NavigationDrivenLocationManager: MLNLocationManager, AnyObject { + var lastLocation: CLLocation { get set } + var lastHeading: CLHeading? { get set } +} + +extension StaticLocationManager: NavigationDrivenLocationManager {} diff --git a/apple/Sources/FerrostarMapLibreUI/Models/NavigationLocationManagerConfiguration.swift b/apple/Sources/FerrostarMapLibreUI/Models/NavigationLocationManagerConfiguration.swift new file mode 100644 index 000000000..040501a3d --- /dev/null +++ b/apple/Sources/FerrostarMapLibreUI/Models/NavigationLocationManagerConfiguration.swift @@ -0,0 +1,25 @@ +import MapLibre +import MapLibreSwiftUI + +/// Configures location managers used by Ferrostar map navigation views. +/// +/// - `nonNavigatingLocationManager`: +/// - `nil` uses MapLibre's default internal manager (recommended default). +/// - any custom manager can be supplied for non-navigation behavior. +/// - `navigatingLocationManager`: +/// - custom manager fed by Ferrostar navigation state. +/// +/// Note: `MLNLocationManager` implementations are reference types. Do not construct this configuration +/// inline in a SwiftUI `body`; keep manager instances in stable state/model storage and pass references here. +public struct NavigationLocationManagerConfiguration { + public var nonNavigatingLocationManager: (any MLNLocationManager)? + public var navigatingLocationManager: any NavigationDrivenLocationManager + + public init( + nonNavigatingLocationManager: (any MLNLocationManager)? = nil, + navigatingLocationManager: any NavigationDrivenLocationManager + ) { + self.nonNavigatingLocationManager = nonNavigatingLocationManager + self.navigatingLocationManager = navigatingLocationManager + } +} diff --git a/apple/Sources/FerrostarMapLibreUI/Views/DynamicallyOrientingNavigationView.swift b/apple/Sources/FerrostarMapLibreUI/Views/DynamicallyOrientingNavigationView.swift index 2fe59994e..1a87a6965 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/DynamicallyOrientingNavigationView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/DynamicallyOrientingNavigationView.swift @@ -14,6 +14,7 @@ public struct DynamicallyOrientingNavigationView: View { let styleURL: URL @Binding var camera: MapViewCamera let navigationCamera: MapViewCamera + let locationManagerConfiguration: NavigationLocationManagerConfiguration? private let navigationState: NavigationState? private let userLayers: [StyleLayerDefinition] @@ -48,6 +49,7 @@ public struct DynamicallyOrientingNavigationView: View { camera: Binding, navigationCamera: MapViewCamera = .automotiveNavigation(), navigationState: NavigationState?, + locationManagerConfiguration: NavigationLocationManagerConfiguration? = nil, isMuted: Bool, minimumSafeAreaInsets: EdgeInsets = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16), onTapMute: @escaping () -> Void, @@ -56,6 +58,7 @@ public struct DynamicallyOrientingNavigationView: View { ) { self.styleURL = styleURL self.navigationState = navigationState + self.locationManagerConfiguration = locationManagerConfiguration self.isMuted = isMuted self.minimumSafeAreaInsets = minimumSafeAreaInsets self.onTapMute = onTapMute @@ -74,6 +77,7 @@ public struct DynamicallyOrientingNavigationView: View { styleURL: styleURL, camera: $camera, navigationState: navigationState, + locationManagerConfiguration: locationManagerConfiguration, onUserTrackingModeChanged: { mode, _ in userTrackingMode = mode }, diff --git a/apple/Sources/FerrostarMapLibreUI/Views/LandscapeNavigationView.swift b/apple/Sources/FerrostarMapLibreUI/Views/LandscapeNavigationView.swift index cfbf11658..714616403 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/LandscapeNavigationView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/LandscapeNavigationView.swift @@ -17,6 +17,7 @@ public struct LandscapeNavigationView: View { let styleURL: URL @Binding var camera: MapViewCamera let navigationCamera: MapViewCamera + let locationManagerConfiguration: NavigationLocationManagerConfiguration? private let navigationState: NavigationState? private let userLayers: [StyleLayerDefinition] @@ -48,6 +49,7 @@ public struct LandscapeNavigationView: View { camera: Binding, navigationCamera: MapViewCamera = .automotiveNavigation(), navigationState: NavigationState?, + locationManagerConfiguration: NavigationLocationManagerConfiguration? = nil, isMuted: Bool, minimumSafeAreaInsets: EdgeInsets = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16), onTapMute: @escaping () -> Void, @@ -56,6 +58,7 @@ public struct LandscapeNavigationView: View { ) { self.styleURL = styleURL self.navigationState = navigationState + self.locationManagerConfiguration = locationManagerConfiguration self.isMuted = isMuted self.minimumSafeAreaInsets = minimumSafeAreaInsets self.onTapMute = onTapMute @@ -73,6 +76,7 @@ public struct LandscapeNavigationView: View { styleURL: styleURL, camera: $camera, navigationState: navigationState, + locationManagerConfiguration: locationManagerConfiguration, onUserTrackingModeChanged: { mode, _ in userTrackingMode = mode }, diff --git a/apple/Sources/FerrostarMapLibreUI/Views/NavigationMapView.swift b/apple/Sources/FerrostarMapLibreUI/Views/NavigationMapView.swift index 8cdcebbe4..222826fbe 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/NavigationMapView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/NavigationMapView.swift @@ -26,8 +26,9 @@ public struct NavigationMapView: View { // TODO: Configurable camera and user "puck" rotation modes private let navigationState: NavigationState? - - @State private var locationManager = StaticLocationManager(initialLocation: CLLocation()) + private let locationManagerConfiguration: NavigationLocationManagerConfiguration? + @State private var defaultNavigatingLocationManager: any NavigationDrivenLocationManager = + StaticLocationManager(initialLocation: CLLocation()) // MARK: Camera Settings @@ -39,12 +40,15 @@ public struct NavigationMapView: View { /// - 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. + /// - locationManagerConfiguration: Optional custom managers for navigating/non-navigating modes. + /// Keep manager instances stable (do not construct inline in `body`). /// - 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, navigationState: NavigationState?, + locationManagerConfiguration: NavigationLocationManagerConfiguration? = nil, activity: MapActivity = .standard, onUserTrackingModeChanged: @escaping (MLNUserTrackingMode, Bool) -> Void = { _, _ in }, onStyleLoaded: @escaping ((MLNStyle) -> Void), @@ -53,6 +57,7 @@ public struct NavigationMapView: View { self.styleURL = styleURL _camera = camera self.navigationState = navigationState + self.locationManagerConfiguration = locationManagerConfiguration self.onUserTrackingModeChanged = onUserTrackingModeChanged self.onStyleLoaded = onStyleLoaded userLayers = makeMapContent() @@ -64,7 +69,7 @@ public struct NavigationMapView: View { MapView( styleURL: styleURL, camera: $camera, - locationManager: locationManager, + locationManager: activeLocationManager, activity: activity ) { // TODO: Create logic and style for route previews. Unless ferrostarCore will handle this internally. @@ -112,11 +117,22 @@ public struct NavigationMapView: View { if let userLocation = navigationState?.preferredUserLocation, // There is no reason to push an update if the coordinate and heading are the same. // That's all that gets displayed, so it's all that MapLibre should care about. - locationManager.lastLocation.coordinate != userLocation.coordinates - .clLocationCoordinate2D || locationManager.lastLocation.course != userLocation.clLocation.course + activeNavigatingLocationManager.lastLocation.coordinate != userLocation.coordinates.clLocationCoordinate2D || + activeNavigatingLocationManager.lastLocation.course != userLocation.clLocation.course { - locationManager.lastLocation = userLocation.clLocation + activeNavigatingLocationManager.lastLocation = userLocation.clLocation + } + } + + private var activeNavigatingLocationManager: any NavigationDrivenLocationManager { + locationManagerConfiguration?.navigatingLocationManager ?? defaultNavigatingLocationManager + } + + private var activeLocationManager: (any MLNLocationManager)? { + if navigationState?.isNavigating == true { + return activeNavigatingLocationManager } + return locationManagerConfiguration?.nonNavigatingLocationManager } } diff --git a/apple/Sources/FerrostarMapLibreUI/Views/PortraitNavigationView.swift b/apple/Sources/FerrostarMapLibreUI/Views/PortraitNavigationView.swift index 5d29ee405..ef087d7c8 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/PortraitNavigationView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/PortraitNavigationView.swift @@ -16,6 +16,7 @@ public struct PortraitNavigationView: View { let styleURL: URL @Binding var camera: MapViewCamera let navigationCamera: MapViewCamera + let locationManagerConfiguration: NavigationLocationManagerConfiguration? private let navigationState: NavigationState? private let userLayers: [StyleLayerDefinition] @@ -47,6 +48,7 @@ public struct PortraitNavigationView: View { camera: Binding, navigationCamera: MapViewCamera = .automotiveNavigation(), navigationState: NavigationState?, + locationManagerConfiguration: NavigationLocationManagerConfiguration? = nil, isMuted: Bool, minimumSafeAreaInsets: EdgeInsets = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16), onTapMute: @escaping () -> Void, @@ -55,6 +57,7 @@ public struct PortraitNavigationView: View { ) { self.styleURL = styleURL self.navigationState = navigationState + self.locationManagerConfiguration = locationManagerConfiguration self.isMuted = isMuted self.minimumSafeAreaInsets = minimumSafeAreaInsets self.onTapMute = onTapMute @@ -73,6 +76,7 @@ public struct PortraitNavigationView: View { styleURL: styleURL, camera: $camera, navigationState: navigationState, + locationManagerConfiguration: locationManagerConfiguration, onUserTrackingModeChanged: { mode, _ in userTrackingMode = mode },