Files
Reflect/Shared/Services/LocationManager.swift
Trey t 78d09803c3 Fix location/weather error handling and complete localization
Add authorization pre-check and 15s timeout to LocationManager to
prevent hanging continuations. WeatherManager now skips retry queue
when location permission is denied. Settings weather toggle shows
alert directing users to Settings when location is denied. Fill
remaining 32 untranslated strings to reach 100% localization.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 17:37:28 -05:00

112 lines
3.6 KiB
Swift

//
// LocationManager.swift
// Reflect
//
// CoreLocation wrapper with async/await for one-shot location requests.
//
import CoreLocation
import os.log
@MainActor
final class LocationManager: NSObject {
static let shared = LocationManager()
private static let logger = Logger(
subsystem: Bundle.main.bundleIdentifier ?? "com.88oakapps.reflect",
category: "LocationManager"
)
private let manager = CLLocationManager()
private var locationContinuation: CheckedContinuation<CLLocation, Error>?
var authorizationStatus: CLAuthorizationStatus {
manager.authorizationStatus
}
private override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyKilometer
}
func requestAuthorization() {
manager.requestWhenInUseAuthorization()
}
var currentLocation: CLLocation {
get async throws {
// Return last known location if recent enough (within 10 minutes)
if let last = manager.location,
abs(last.timestamp.timeIntervalSinceNow) < 600 {
return last
}
// Pre-check authorization before requesting location
switch manager.authorizationStatus {
case .denied, .restricted:
throw LocationError.permissionDenied
case .notDetermined:
manager.requestWhenInUseAuthorization()
// Wait briefly for authorization response
try await Task.sleep(for: .seconds(1))
if manager.authorizationStatus == .denied || manager.authorizationStatus == .restricted {
throw LocationError.permissionDenied
}
default:
break
}
// Request location with a 15-second timeout
return try await withCheckedThrowingContinuation { continuation in
self.locationContinuation = continuation
self.manager.requestLocation()
// Schedule timeout safe because everything is @MainActor (no race)
Task { @MainActor in
try? await Task.sleep(for: .seconds(15))
guard self.locationContinuation != nil else { return }
self.locationContinuation?.resume(throwing: LocationError.timeout)
self.locationContinuation = nil
}
}
}
}
// MARK: - Errors
enum LocationError: LocalizedError {
case permissionDenied
case timeout
var errorDescription: String? {
switch self {
case .permissionDenied:
return "Location permission denied"
case .timeout:
return "Location request timed out"
}
}
}
}
// MARK: - CLLocationManagerDelegate
extension LocationManager: CLLocationManagerDelegate {
nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
Task { @MainActor in
guard let location = locations.first else { return }
locationContinuation?.resume(returning: location)
locationContinuation = nil
}
}
nonisolated func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
Task { @MainActor in
Self.logger.error("Location request failed: \(error.localizedDescription)")
locationContinuation?.resume(throwing: error)
locationContinuation = nil
}
}
}