iOS SwiftUI Apple Maps: Show User Location and Prevent Map Annotations from Rotating with the Map

While working on an iOS app that uses Apple Maps, I was surprised to discover that there’s no built-in option to prevent map annotations (markers) from rotating with the map, nor is there a standard way to keep the user’s location and heading visible at all times.

This short blog post demonstrates a practical approach to always displaying the user’s position and showing device rotation alongside it.

import SwiftUI
import SwiftData
import MapKit
import CoreLocation

struct AppleMapDemoView: View {
        
    @State private var mapCameraPosition: MapCameraPosition = .userLocation(fallback: .automatic)
    @State private var model = AppleMapDemoViewModel()
    @State private var cameraHeading: Double = 0
    
    var body: some View {
        VStack {
            Map(position: $mapCameraPosition) {
                
                // User Annotation is empty to prevent the standard user marker from being displayed
                UserAnnotation { }
                
                if let userLocation = model.deviceLocation?.coordinate {
                    Annotation("User", coordinate: userLocation) {
                        Image(systemName: "arrow.up")
                            .rotationEffect(.degrees((model.deviceHeading?.trueHeading ?? 0.0) - cameraHeading))
                    }
                }
                
                
            }
            .onMapCameraChange(frequency: .continuous) { context in
                cameraHeading = context.camera.heading
            }
            .mapStyle(.standard)
            
        }
        .onAppear {
            model.start()
        }
        .onDisappear {
            model.stop()
        }
    }
    
}

@Observable
class AppleMapDemoViewModel: NSObject, CLLocationManagerDelegate {
    
    private let locationManager = CLLocationManager()
    
    private(set) var state: State = .notInitialized
    private(set) var authorizationStatus: CLAuthorizationStatus = .notDetermined
    private(set) var deviceHeading: CLHeading?
    private(set) var deviceLocation: CLLocation?
    
    
    override init() {
        super.init()
        locationManager.delegate = self
    }
    
    func start() {
        locationManager.requestWhenInUseAuthorization()
        state = .authorizationRequested
    }
    
    private func startLocationUpdates() {
        state = .active
        locationManager.startUpdatingLocation()
        locationManager.startUpdatingHeading()
    }
    
    func stop() {
        locationManager.stopUpdatingLocation()
        locationManager.stopUpdatingHeading()
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        if let location = locations.first {
            deviceLocation = location
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
        deviceHeading = newHeading
    }
    
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        self.authorizationStatus = status
        switch status {
            case .authorizedAlways, .authorizedWhenInUse, .authorized:
                startLocationUpdates()
                break
            default:
                state = .authorizationDenied
                break
        }
    }
    
    enum State {
        case notInitialized
        case authorizationRequested
        case active
        case authorizationDenied
    }
        
}

How It Works

In this example, the ViewModel uses CLLocationManager to receive updates about the device’s location and heading.

We also track changes to the map camera and store the camera’s heading angle.

The View then displays the user’s marker (represented here as an arrow) and updates its rotation by combining the map’s rotation with the device’s heading, ensuring the marker always points in the correct direction.

To prevent annotations (map markers) from rotating with the map, simply omit the device heading from this calculation.

Note: This is a simplified example. In real world:

  • Location authorization should be managed in a dedicated manager.
  • The UI should provide feedback if authorization is denied.
  • Screen orientation (portrait top/bottom, landscape left/right) should also be taken into account.
  • Etc.

Technologies Used: iOS, Swift, SwiftUI, Apple Maps