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