iOS SwiftUI Apple Maps: Wyświetlanie lokalizacji użytkownika i zapobieganie obracaniu się markerów wraz z mapą

Podczas pracy nad aplikacją iOS wykorzystującą Apple Maps byłem zaskoczony, że nie istnieje wbudowana opcja, która zapobiegałaby obracaniu się adnotacji (markerów) wraz z mapą, ani istnieje standardowy sposób na stałe wyświetlanie lokalizacji użytkownika i kierunku urządzenia.
Ten krótki wpis pokazuje praktyczne rozwiązanie, jak zawsze prezentować pozycję użytkownika biorąc pod uwagę rotację urządzenia.

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
    }
        
}

Jak to działa

W tym przykładzie ViewModel wykorzystuje CLLocationManager, aby otrzymywać aktualizacje dotyczące lokalizacji i kierunku urządzenia.

Śledzimy także zmiany kamery mapy i zapisujemy kąt jej obrotu.

View wyświetla następnie marker użytkownika (w tym przypadku w formie strzałki) i aktualizuje jego obrót, łącząc rotację mapy z kierunkiem urządzenia, aby strzałka zawsze wskazywała właściwy kierunek.

Aby zapobiec obracaniu się adnotacji (markerów) wraz z mapą, wystarczy pominąć kąt obrotu urządzenia w tym obliczeniu.

Uwaga: To jest uproszczony przykład. W prawdziwej aplikacji:

  • Autoryzacja lokalizacji powinna być obsługiwana w dedykowanym menedżerze.
  • UI powinno przekazywać informację zwrotną w przypadku odmowy autoryzacji.
  • Należy uwzględnić orientację ekranu (pionowa góra/dół, pozioma lewa/prawa).
  • Itd.

Użyte technologie: iOS, Swift, SwiftUI, Apple Maps