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..= 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)") } }