73 lines
2.6 KiB
Swift
73 lines
2.6 KiB
Swift
// WidgetLocationFetcher.swift — one-shot location for the WIDGET EXTENSION.
|
|
//
|
|
// WidgetKit providers run in a short budget, so this fetcher is deliberately
|
|
// timeout-bounded: it requests a single location and gives up after a few
|
|
// seconds, falling back to whatever the caller already has cached.
|
|
//
|
|
// Location permission is shared with the containing app (same TCC identity),
|
|
// so if the user granted "While Using" in FuelBoard, the widget can use
|
|
// location directly — no extra prompt in the car.
|
|
|
|
import Foundation
|
|
import CoreLocation
|
|
|
|
@MainActor
|
|
final class WidgetLocationFetcher: NSObject, CLLocationManagerDelegate {
|
|
static let shared = WidgetLocationFetcher()
|
|
|
|
private let manager = CLLocationManager()
|
|
private var continuation: CheckedContinuation<CLLocation?, Never>?
|
|
private var timeoutTask: Task<Void, Never>?
|
|
|
|
private override init() {
|
|
super.init()
|
|
manager.delegate = self
|
|
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
|
|
}
|
|
|
|
/// Requests the current location. Returns nil on denial, timeout, or
|
|
/// no-permission — callers should fall back to cached data.
|
|
func currentLocation(timeout: TimeInterval = 4) async -> CLLocation? {
|
|
guard CLLocationManager.locationServicesEnabled() else { return nil }
|
|
|
|
switch manager.authorizationStatus {
|
|
case .authorizedWhenInUse, .authorizedAlways:
|
|
break
|
|
case .notDetermined:
|
|
// Widget extensions share the app's TCC identity; if the app was
|
|
// never opened, the system can't prompt from here — fall back.
|
|
return nil
|
|
case .denied, .restricted:
|
|
return nil
|
|
@unknown default:
|
|
return nil
|
|
}
|
|
|
|
return await withCheckedContinuation { cont in
|
|
continuation = cont
|
|
manager.requestLocation()
|
|
timeoutTask = Task { [weak self] in
|
|
try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
|
|
self?.continuation?.resume(returning: nil)
|
|
self?.continuation = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
MainActor.assumeIsolated {
|
|
timeoutTask?.cancel()
|
|
continuation?.resume(returning: locations.last)
|
|
continuation = nil
|
|
}
|
|
}
|
|
|
|
nonisolated func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
MainActor.assumeIsolated {
|
|
timeoutTask?.cancel()
|
|
continuation?.resume(returning: nil)
|
|
continuation = nil
|
|
}
|
|
}
|
|
}
|