- Remove all print statements from planning engine, data providers, and PDF generation - Fix deprecated CLGeocoder usage with MKLocalSearch for iOS 26 - Fix Swift 6 actor isolation by converting PDFGenerator/ExportService to @MainActor - Add @retroactive to CLLocationCoordinate2D protocol conformances - Fix unused variable warnings in GameDAGRouter and scenario planners - Remove unreachable catch blocks in SettingsViewModel 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
217 lines
6.8 KiB
Swift
217 lines
6.8 KiB
Swift
//
|
|
// LocationPermissionManager.swift
|
|
// SportsTime
|
|
//
|
|
// Manages location permission requests and status
|
|
//
|
|
|
|
import Foundation
|
|
import CoreLocation
|
|
import SwiftUI
|
|
|
|
@MainActor
|
|
@Observable
|
|
final class LocationPermissionManager: NSObject {
|
|
static let shared = LocationPermissionManager()
|
|
|
|
private(set) var authorizationStatus: CLAuthorizationStatus = .notDetermined
|
|
private(set) var currentLocation: CLLocation?
|
|
private(set) var isRequestingPermission = false
|
|
|
|
private let locationManager = CLLocationManager()
|
|
|
|
override private init() {
|
|
super.init()
|
|
locationManager.delegate = self
|
|
authorizationStatus = locationManager.authorizationStatus
|
|
}
|
|
|
|
// MARK: - Computed Properties
|
|
|
|
var isAuthorized: Bool {
|
|
authorizationStatus == .authorizedWhenInUse || authorizationStatus == .authorizedAlways
|
|
}
|
|
|
|
var needsPermission: Bool {
|
|
authorizationStatus == .notDetermined
|
|
}
|
|
|
|
var isDenied: Bool {
|
|
authorizationStatus == .denied || authorizationStatus == .restricted
|
|
}
|
|
|
|
var statusMessage: String {
|
|
switch authorizationStatus {
|
|
case .notDetermined:
|
|
return "Location access helps find nearby stadiums and optimize your route."
|
|
case .restricted:
|
|
return "Location access is restricted on this device."
|
|
case .denied:
|
|
return "Location access was denied. Enable it in Settings to use this feature."
|
|
case .authorizedAlways, .authorizedWhenInUse:
|
|
return "Location access granted."
|
|
@unknown default:
|
|
return "Unknown location status."
|
|
}
|
|
}
|
|
|
|
// MARK: - Actions
|
|
|
|
func requestPermission() {
|
|
guard authorizationStatus == .notDetermined else { return }
|
|
isRequestingPermission = true
|
|
locationManager.requestWhenInUseAuthorization()
|
|
}
|
|
|
|
func requestCurrentLocation() {
|
|
guard isAuthorized else { return }
|
|
locationManager.requestLocation()
|
|
}
|
|
|
|
func openSettings() {
|
|
guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { return }
|
|
UIApplication.shared.open(settingsURL)
|
|
}
|
|
}
|
|
|
|
// MARK: - CLLocationManagerDelegate
|
|
|
|
extension LocationPermissionManager: CLLocationManagerDelegate {
|
|
nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
|
Task { @MainActor in
|
|
self.authorizationStatus = manager.authorizationStatus
|
|
self.isRequestingPermission = false
|
|
|
|
// Auto-request location if newly authorized
|
|
if self.isAuthorized {
|
|
self.requestCurrentLocation()
|
|
}
|
|
}
|
|
}
|
|
|
|
nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
Task { @MainActor in
|
|
self.currentLocation = locations.last
|
|
}
|
|
}
|
|
|
|
nonisolated func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
// Location error handled silently
|
|
}
|
|
}
|
|
|
|
// MARK: - Location Permission View
|
|
|
|
struct LocationPermissionView: View {
|
|
@Bindable var manager = LocationPermissionManager.shared
|
|
|
|
var body: some View {
|
|
VStack(spacing: 16) {
|
|
Image(systemName: "location.circle.fill")
|
|
.font(.system(size: 60))
|
|
.foregroundStyle(.blue)
|
|
|
|
Text("Enable Location")
|
|
.font(.title2)
|
|
.fontWeight(.bold)
|
|
|
|
Text(manager.statusMessage)
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal)
|
|
|
|
if manager.needsPermission {
|
|
Button {
|
|
manager.requestPermission()
|
|
} label: {
|
|
Text("Allow Location Access")
|
|
.fontWeight(.semibold)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
.background(Color.blue)
|
|
.foregroundStyle(.white)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
.padding(.horizontal)
|
|
} else if manager.isDenied {
|
|
Button {
|
|
manager.openSettings()
|
|
} label: {
|
|
Text("Open Settings")
|
|
.fontWeight(.semibold)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
.background(Color.blue)
|
|
.foregroundStyle(.white)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
.padding(.horizontal)
|
|
} else if manager.isAuthorized {
|
|
Label("Location Enabled", systemImage: "checkmark.circle.fill")
|
|
.foregroundStyle(.green)
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
}
|
|
|
|
// MARK: - Location Permission Banner
|
|
|
|
struct LocationPermissionBanner: View {
|
|
@Bindable var manager = LocationPermissionManager.shared
|
|
@Binding var isPresented: Bool
|
|
|
|
var body: some View {
|
|
if manager.needsPermission || manager.isDenied {
|
|
HStack(spacing: 12) {
|
|
Image(systemName: "location.slash")
|
|
.foregroundStyle(.orange)
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text("Location Not Available")
|
|
.font(.subheadline)
|
|
.fontWeight(.medium)
|
|
Text(manager.needsPermission ? "Enable for better route planning" : "Tap to enable in Settings")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Button {
|
|
if manager.needsPermission {
|
|
manager.requestPermission()
|
|
} else {
|
|
manager.openSettings()
|
|
}
|
|
} label: {
|
|
Text(manager.needsPermission ? "Enable" : "Settings")
|
|
.font(.caption)
|
|
.fontWeight(.semibold)
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 6)
|
|
.background(Color.blue)
|
|
.foregroundStyle(.white)
|
|
.clipShape(Capsule())
|
|
}
|
|
|
|
Button {
|
|
isPresented = false
|
|
} label: {
|
|
Image(systemName: "xmark")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.padding()
|
|
.background(Color(.secondarySystemBackground))
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
LocationPermissionView()
|
|
}
|