|
import SwiftUI |
|
import Foundation |
|
import CoreLocation |
|
import Combine |
|
|
|
class LocationManager: ObservableObject { |
|
enum LocationManagerError: Error, LocalizedError { |
|
case deniedLocation; case restrictedLocation; case unknown |
|
var errorDescription: String? { |
|
switch self { |
|
case .deniedLocation: return "Location information is not allowed. Please allow Settings - Privacy to retrieve the location of your app." |
|
case .restrictedLocation: return "Location information is not allowed by the constraints specified on the device." |
|
case .unknown: return "An unknown error has occurred." |
|
} |
|
} |
|
} |
|
typealias AuthorizationStatusContinuation = CheckedContinuation<CLAuthorizationStatus, Never> // Continuation for asymc/await |
|
fileprivate class DelegateAdaptorForAuthorization: NSObject, CLLocationManagerDelegate { |
|
var continuation: AuthorizationStatusContinuation? |
|
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { |
|
continuation?.resume(returning: manager.authorizationStatus) |
|
} |
|
} |
|
fileprivate class DelegateAdaptorForLocation: NSObject, CLLocationManagerDelegate { |
|
var currentLocation: PassthroughSubject<CurrentLocationStatus, Never> = .init() |
|
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { |
|
guard let newLocation = locations.last else { return } |
|
currentLocation.send(CurrentLocationStatus(active: true, location: newLocation)) |
|
} |
|
} |
|
fileprivate let delegateAdaptorForLocation: DelegateAdaptorForLocation = .init() |
|
fileprivate let locationManager: CLLocationManager = .init() |
|
@Published var currentLocation: CurrentLocationStatus = .init(active: false, location: .init()) |
|
|
|
struct CurrentLocationStatus: Equatable { |
|
var active: Bool; var location: CLLocation |
|
} |
|
var anyCancelableCollection = Set<AnyCancellable>() |
|
|
|
func startCurrentLocation() async throws { |
|
let authorizationStatus: CLAuthorizationStatus |
|
if locationManager.authorizationStatus == .notDetermined { |
|
let delegateAdaptor = DelegateAdaptorForAuthorization() |
|
locationManager.delegate = delegateAdaptor |
|
authorizationStatus = await withCheckedContinuation { (continuation: AuthorizationStatusContinuation) in |
|
delegateAdaptor.continuation = continuation |
|
locationManager.requestWhenInUseAuthorization() |
|
} |
|
} else { |
|
authorizationStatus = locationManager.authorizationStatus |
|
} |
|
locationManager.delegate = delegateAdaptorForLocation |
|
delegateAdaptorForLocation.currentLocation.sink { [unowned self] location in |
|
if self.currentLocation.location.distance(from: location.location) < 5 /*distance 5metre over*/ { |
|
return |
|
} |
|
self.currentLocation = location |
|
}.store(in: &anyCancelableCollection) |
|
locationManager.startUpdatingLocation() |
|
|
|
switch authorizationStatus { |
|
case .notDetermined: break |
|
case .denied: throw LocationManagerError.deniedLocation |
|
case .authorizedAlways, .authorizedWhenInUse: break |
|
case .restricted: throw LocationManagerError.restrictedLocation |
|
default: throw LocationManagerError.unknown |
|
} |
|
} |
|
} |
|
|
|
@main |
|
struct SimpleLocationAuthorizationApp: App { |
|
@StateObject var locationManager: LocationManager = .init() |
|
var body: some Scene { |
|
WindowGroup { |
|
ContentView(locationManager: locationManager) |
|
} |
|
} |
|
} |
|
struct ContentView: View { |
|
@ObservedObject var locationManager: LocationManager |
|
@State var errorMessage: String = "" |
|
@State var showErrorAlert: Bool = false |
|
var body: some View { |
|
VStack { |
|
Image(systemName: "location.circle.fill").imageScale(.large).foregroundColor(.accentColor) |
|
Text("Obtaining location information...") |
|
}.task { |
|
do { |
|
try await locationManager.startCurrentLocation() |
|
} catch let error { |
|
errorMessage = error.localizedDescription |
|
showErrorAlert = true |
|
} |
|
}.alert(errorMessage, isPresented: $showErrorAlert) { |
|
Button("OK", role: .cancel, action: { errorMessage = ""}) |
|
} |
|
} |
|
} |