SwiftUI Map with MapCamera jerks on every GPS update instead of animating smoothly
I'm trying to make camera to follow the user smoothly during navigation using MapCamera with heading and pitch, similar to Apple Maps or Google Maps. The camera updates on every GPS tick but instead of animating smoothly between positions it jerks , it snaps to the new position, pauses, snaps again, pauses...terrible UX.
The blue user location (UserAnnotation) puck moves completely smoothly. Only the camera jerks
I have tried all sort of animations and interpolation you may think of. Something is just not right, must be something missing from the puzzle. I have prepared a minimal reproducible example so you can copy and paste the only thing needed is to add the
-
Privacy - Location When In Use Usage Description
-
Run in Simulator, go to Features > Location > Freeway Drive and tap on Track then you'll notice how camera is following then stop then following and stops again
Don't bother using AI, he has no clue what's this all about. I also went through docs to find anything useful like a magic modifier, but no joy
Here is a video hosted online as well:
[https://streamable.com/ear9cv]
And a code snippet copy paste
import MapKit
import CoreLocation
// You'll only need to add Privacy - Location When In Use Usage Description to the Info tab
struct ContentView: View {
@State private var locationManager = LocationManagerDelegate()
@State private var cameraPosition: MapCameraPosition = .userLocation(followsHeading: false, fallback: .automatic)
@State private var isTracking: Bool = false
@State private var lastKnownHeading: Double = 0
var body: some View {
Map(position: $cameraPosition) {
UserAnnotation()
}
.onChange(of: locationManager.location) { _, location in
guard isTracking, let location else { return }
withAnimation(.linear(duration: 0.5)) {
cameraPosition = .camera(MapCamera(
centerCoordinate: location.coordinate,
distance: 1000,
heading: location.course,
pitch: 60
))
}
}
.safeAreaInset(edge: .bottom) {
// Added to the safeAreaInset to keep the Apple Logo visible
Button("Track") {
isTracking.toggle()
locationManager.requestPermission()
locationManager.startNavigating()
}
.buttonStyle(.glassProminent)
.buttonSizing(.flexible)
.controlSize(.extraLarge)
.padding(.horizontal)
}
}
}
@MainActor
@Observable
final class LocationManagerDelegate: NSObject, CLLocationManagerDelegate {
var location: CLLocation?
var authorizationStatus: CLAuthorizationStatus = .notDetermined
let manager = CLLocationManager()
private var liveUpdateTask: Task<Void, Never>?
override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
authorizationStatus = manager.authorizationStatus
}
func requestPermission() { manager.requestWhenInUseAuthorization() }
func startNavigating() {
liveUpdateTask = Task {
do {
for try await update in CLLocationUpdate.liveUpdates(.automotiveNavigation) {
guard let newLocation = update.location else { continue }
self.location = newLocation
}
} catch {
print("Live updates error: \(error)")
}
}
}
func stopNavigating() {
liveUpdateTask?.cancel()
liveUpdateTask = nil
manager.requestLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
location = locations.last
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
authorizationStatus = manager.authorizationStatus
}
}