chore: wire in location services

Signed-off-by: Alan Brault <alan.brault@visus.io>
This commit is contained in:
2026-08-09 10:22:38 -04:00
parent 59cb2737a1
commit e1da561ff6
9 changed files with 256 additions and 9 deletions
@@ -0,0 +1,169 @@
import CoreLocation
import TempestasDomain
@MainActor
public final class DefaultLocationService: NSObject, LocationService {
private let manager = CLLocationManager()
private let geocoder = CLGeocoder()
private var authorizationContinuation: CheckedContinuation<Void, Never>?
private var locationContinuation: CheckedContinuation<CLLocation, Error>?
private var authorizationTask: Task<Void, Never>?
private var locationTask: Task<CurrentLocation, Error>?
override public init() {
super.init()
manager.delegate = self
}
public func requestAuthorization() async {
guard manager.authorizationStatus == .notDetermined else { return }
if let authorizationTask {
return await authorizationTask.value
}
let task: Task<Void, Never> = Task { @MainActor in
await self.awaitAuthorizationChange()
}
authorizationTask = task
defer { authorizationTask = nil }
await withTaskCancellationHandler {
await task.value
} onCancel: {
task.cancel()
}
}
public func getCurrentLocation() async throws -> CurrentLocation {
if let locationTask {
return try await locationTask.value
}
let task: Task<CurrentLocation, Error> = Task { @MainActor in
try await self.fetchCurrentLocation()
}
locationTask = task
defer { locationTask = nil }
return try await withTaskCancellationHandler {
try await task.value
} onCancel: {
task.cancel()
}
}
private func fetchCurrentLocation() async throws -> CurrentLocation {
try await ensureAuthorized()
let location = try await requestLocationOnce()
let name = await resolveName(for: location)
return CurrentLocation(
coordinate: Coordinate(
latitude: location.coordinate.latitude,
longitude: location.coordinate.longitude
),
name: name
)
}
private func ensureAuthorized() async throws {
await requestAuthorization()
switch manager.authorizationStatus {
case .authorizedAlways:
return
case .notDetermined, .denied, .restricted:
throw LocationServiceError.authorizationDenied
@unknown default:
throw LocationServiceError.authorizationDenied
}
}
private func awaitAuthorizationChange() async {
await withTaskCancellationHandler {
await withCheckedContinuation { continuation in
self.authorizationContinuation = continuation
self.manager.requestWhenInUseAuthorization()
}
} onCancel: {
Task { @MainActor in
self.cancelPendingAuthorizationWait()
}
}
}
private func cancelPendingAuthorizationWait() {
guard let authorizationContinuation else { return }
self.authorizationContinuation = nil
authorizationContinuation.resume()
}
private func requestLocationOnce() async throws -> CLLocation {
try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
self.locationContinuation = continuation
self.manager.requestLocation()
}
} onCancel: {
Task { @MainActor in
self.cancelPendingLocationRequest()
}
}
}
private func cancelPendingLocationRequest() {
guard let locationContinuation else { return }
self.locationContinuation = nil
manager.stopUpdatingLocation()
locationContinuation.resume(throwing: CancellationError())
}
private func resolveName(for location: CLLocation) async -> String {
guard let placemarks = try? await geocoder.reverseGeocodeLocation(location),
let placemark = placemarks.first else {
return Self.fallbackName(for: location)
}
return placemark.locality ?? placemark.name ?? Self.fallbackName(for: location)
}
private static func fallbackName(for location: CLLocation) -> String {
String(format: "%.4f, %.4f", location.coordinate.latitude, location.coordinate.longitude)
}
}
extension DefaultLocationService: @MainActor CLLocationManagerDelegate {
public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
guard let authorizationContinuation else { return }
self.authorizationContinuation = nil
authorizationContinuation.resume()
}
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let locationContinuation else { return }
self.locationContinuation = nil
guard let location = locations.last else {
locationContinuation.resume(
throwing: LocationServiceError.locationUnavailable(reason: "No location was returned")
)
return
}
locationContinuation.resume(returning: location)
}
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
guard let locationContinuation else { return }
self.locationContinuation = nil
locationContinuation.resume(
throwing: LocationServiceError.locationUnavailable(reason: error.localizedDescription)
)
}
}