Files
fuelboard/Shared/WidgetLocationFetcher.swift
T

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
}
}
}