- BanYaroGo-Info.plist (explizit, statt INFOPLIST_KEY_*): UIBackgroundModes location, NSLocationWhenInUseUsageDescription, NSLocationAlwaysAndWhenInUseUsageDescription - LocationTracker: CLLocationManager-Wrapper (@Observable @MainActor), Distanz via CLLocation.distance, Permission-Handling, Background-Updates - RouteCreateBody + Encoder mit convertToSnakeCase für POST /api/routes - TrackingView: Start-Hero-Screen + Live-Karte mit MapPolyline + Stats-Karte - FinishWalkSheet: Name + Hunde-Multiselect + POST /api/routes - MainTabView: neuer Aufnehmen-Tab zwischen Touren und Hunde - AppIcon: neues Hund-mit-GPS-Pin (vom User bereitgestellt, weiße Ränder weggeschnitten + Ecken mit Hintergrundfarbe gefüllt)
127 lines
4.3 KiB
Swift
127 lines
4.3 KiB
Swift
import Foundation
|
|
import Observation
|
|
import CoreLocation
|
|
|
|
/// Wraps CLLocationManager for live Gassi-Tracking with background updates.
|
|
///
|
|
/// Usage:
|
|
/// 1. `startOrRequest()` — handles permission flow + starts recording when granted.
|
|
/// 2. Watch `isTracking`, `points`, `totalDistanceMeters`, `startedAt`.
|
|
/// 3. `stop()` to end the recording and inspect the final `points`.
|
|
@Observable
|
|
@MainActor
|
|
final class LocationTracker: NSObject, CLLocationManagerDelegate {
|
|
private let manager = CLLocationManager()
|
|
|
|
var points: [GPSPoint] = []
|
|
var isTracking: Bool = false
|
|
var startedAt: Date?
|
|
var authorizationStatus: CLAuthorizationStatus
|
|
var permissionDenied: Bool = false
|
|
|
|
/// Set when the user asked to start but we're still waiting for the system
|
|
/// permission prompt. The delegate auto-starts as soon as access is granted.
|
|
private var pendingStart: Bool = false
|
|
|
|
override init() {
|
|
self.authorizationStatus = manager.authorizationStatus
|
|
super.init()
|
|
manager.delegate = self
|
|
manager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
|
|
manager.distanceFilter = 5 // meters
|
|
manager.activityType = .fitness
|
|
manager.pausesLocationUpdatesAutomatically = false
|
|
}
|
|
|
|
// MARK: - Public API
|
|
|
|
func startOrRequest() {
|
|
permissionDenied = false
|
|
switch authorizationStatus {
|
|
case .notDetermined:
|
|
pendingStart = true
|
|
manager.requestWhenInUseAuthorization()
|
|
case .denied, .restricted:
|
|
permissionDenied = true
|
|
case .authorizedWhenInUse, .authorizedAlways:
|
|
beginTracking()
|
|
@unknown default:
|
|
permissionDenied = true
|
|
}
|
|
}
|
|
|
|
func stop() {
|
|
manager.stopUpdatingLocation()
|
|
manager.allowsBackgroundLocationUpdates = false
|
|
isTracking = false
|
|
}
|
|
|
|
/// Total distance walked so far, in meters.
|
|
var totalDistanceMeters: Double {
|
|
guard points.count >= 2 else { return 0 }
|
|
var total: Double = 0
|
|
for i in 1..<points.count {
|
|
let a = CLLocation(latitude: points[i - 1].lat, longitude: points[i - 1].lon)
|
|
let b = CLLocation(latitude: points[i].lat, longitude: points[i].lon)
|
|
total += b.distance(from: a)
|
|
}
|
|
return total
|
|
}
|
|
|
|
// MARK: - Internal
|
|
|
|
private func beginTracking() {
|
|
points = []
|
|
startedAt = .now
|
|
manager.allowsBackgroundLocationUpdates = true
|
|
manager.showsBackgroundLocationIndicator = true
|
|
manager.startUpdatingLocation()
|
|
isTracking = true
|
|
|
|
// If we only have "when in use", silently try to escalate to "always".
|
|
// System will show the secondary prompt on its own schedule.
|
|
if authorizationStatus == .authorizedWhenInUse {
|
|
manager.requestAlwaysAuthorization()
|
|
}
|
|
}
|
|
|
|
// MARK: - CLLocationManagerDelegate
|
|
|
|
nonisolated func locationManager(
|
|
_ manager: CLLocationManager,
|
|
didUpdateLocations locations: [CLLocation]
|
|
) {
|
|
let newPoints = locations
|
|
.filter { $0.horizontalAccuracy >= 0 && $0.horizontalAccuracy < 30 }
|
|
.map { GPSPoint(lat: $0.coordinate.latitude, lon: $0.coordinate.longitude, alt: $0.altitude) }
|
|
guard !newPoints.isEmpty else { return }
|
|
Task { @MainActor in
|
|
self.points.append(contentsOf: newPoints)
|
|
}
|
|
}
|
|
|
|
nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
|
let status = manager.authorizationStatus
|
|
Task { @MainActor in
|
|
self.authorizationStatus = status
|
|
switch status {
|
|
case .denied, .restricted:
|
|
self.pendingStart = false
|
|
self.permissionDenied = true
|
|
case .authorizedWhenInUse, .authorizedAlways:
|
|
if self.pendingStart {
|
|
self.pendingStart = false
|
|
self.beginTracking()
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
nonisolated func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
// Transient errors (e.g. no fix yet) are common — we just keep listening.
|
|
// Log to console so we can see during development.
|
|
print("LocationTracker error: \(error)")
|
|
}
|
|
}
|