Initial commit: SportsTime trip planning app
- Three-scenario planning engine (A: date range, B: selected games, C: directional routes) - GeographicRouteExplorer with anchor game support for route exploration - Shared ItineraryBuilder for travel segment calculation - TravelEstimator for driving time/distance estimation - SwiftUI views for trip creation and detail display - CloudKit integration for schedule data - Python scraping scripts for sports schedules 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
218
SportsTime/Core/Services/LocationPermissionManager.swift
Normal file
218
SportsTime/Core/Services/LocationPermissionManager.swift
Normal file
@@ -0,0 +1,218 @@
|
||||
//
|
||||
// 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) {
|
||||
Task { @MainActor in
|
||||
print("Location error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
Reference in New Issue
Block a user