chore: wire in location services
Signed-off-by: Alan Brault <alan.brault@visus.io>
This commit is contained in:
+169
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user