import Foundation import CoreLocation import SwiftLocation enum LocationError: LocalizedError { case generic case permission case reverseGeocode var errorDescription: String? { switch self { case .generic: "Location error" case .permission: "Location permission error" case .reverseGeocode: "Reverse geocode error" } } } @MainActor public class RxLocationManager { private let generalErrors: [CLError.Code] = [.locationUnknown, .denied, .network, .headingFailure, .rangingUnavailable, .rangingFailure] private let geocodingErrors: [CLError.Code] = [.geocodeCanceled, .geocodeFoundNoResult, .geocodeFoundPartialResult] private static let locationManager: Location = { let manger = CLLocationManager() manger.desiredAccuracy = kCLLocationAccuracyBest return Location(locationManager: manger) }() private static var eventTask: Task? public private(set) static var lastEvent: VehicleEventDto? private static func checkPermissions() async throws { switch locationManager.authorizationStatus { case .authorizedWhenInUse, .authorizedAlways: break case .notDetermined: let status = try await locationManager.requestPermission(.always) if [.authorizedWhenInUse, .authorizedAlways].contains(status) { return } else { throw LocationError.permission } case .denied: throw CLError(.denied) default: throw LocationError.permission } } private static func requestLocation() async throws -> VehicleEventDto { let locationEvent = try await locationManager.requestLocation(timeout: 20) guard let coordinate = locationEvent.location?.coordinate else { throw LocationError.generic } let event = VehicleEventDto(lat: coordinate.latitude, lon: coordinate.longitude) self.lastEvent = event return event } @discardableResult public static func requestCurrentLocation() async throws -> VehicleEventDto { if let eventTask { return try await eventTask.value } else { try await checkPermissions() let task = Task { let location = try await requestLocation() eventTask = nil return location } eventTask = task return try await task.value } } public static func locationRequestInProgress() -> Bool { return self.eventTask != nil } public static func getAddressForLocation(latitude: Double, longitude: Double) async throws -> String { try await withCheckedThrowingContinuation { continuation in let geocoder = CLGeocoder() let location = CLLocation(latitude: latitude, longitude: longitude) geocoder.reverseGeocodeLocation(location) { placemarks, error in if let error = error { continuation.resume(throwing: error) } else if let placemark = placemarks?.first, let name = placemark.name { continuation.resume(returning: name) } else { continuation.resume(throwing: LocationError.reverseGeocode) } } // TODO: Add cancellation after timeout (20 seconds) //geocoder.cancelGeocode() } } public static func resetLastEvent() { self.lastEvent = nil } public static func getLastEvent() async -> VehicleEventDto? { lastEvent } }