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>
112 lines
3.6 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|