I have the following code:
class Locator : NSObject, ObservableObject
{
private let locationManager: CLLocationManager
private var authorizationContinuation: CheckedContinuation<CLAuthorizationStatus, Never>?
@Published var authorizationStatus: CLAuthorizationStatus
@Published var location: CLLocation?
@Published var error: Error?
override init()
{
locationManager = CLLocationManager()
authorizationStatus = locationManager.authorizationStatus
super.init()
locationManager.delegate = self
}
@MainActor func checkAuthorizationStatus() async -> CLAuthorizationStatus
{
let status = locationManager.authorizationStatus
if status == .notDetermined
{
return await withCheckedContinuation
{ continuation in
authorizationContinuation = continuation
locationManager.requestWhenInUseAuthorization()
}
}
else
{
authorizationStatus = status
return status
}
}
}
extension Locator : CLLocationManagerDelegate
{
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager)
{
authorizationStatus = manager.authorizationStatus
authorizationContinuation?.resume(returning: authorizationStatus)
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error)
{
print(error)
self.error = error
location = nil
}
}
The function checkAuthorizationStatus()
stores state in the form of authorizationContinuation
.
If checkAuthorizationStatus()
would be called a second time before authorizationContinuation?.resume(returning: authorizationStatus)
, this state will be overwritten and the first async call would never resume.
Is it possible that checkAuthorizationStatus()
is called multiple times and that this state is overwritten? If so, can it be prevented, or is there some way around it?
In imperative codebase (e.g., UIKit), AsyncStream
is the natural pattern to wrap the asynchronous sequence of authorization (and location) events. Perhaps something like this gist.
But when dealing with a codebase following declarative patterns (i.e., observable objects), one can simply observe the @Published
value, authorizationStatus
and you are done. No need to await checkAuthorizationStatus
(cutting the Gordian knot with respect to this saving/overwriting of the continuation).
So, request authorization if not already determined, but you don't need to make this an asynchronous event, as your observed property will automatically propagate authorization status changes to your UI already.
And, as discussed elsewhere, we would generally put the class with the location manager on the main actor (because, amongst other considerations, the documentation says CLLocationManager
needs a runloop):
@MainActor
class ViewModel: NSObject, ObservableObject {
let locationManager = CLLocationManager()
@Published var error: Error?
@Published var location: CLLocation?
@Published var authorizationStatus: CLAuthorizationStatus = .notDetermined
override init() {
super.init()
locationManager.delegate = self
authorizationStatus = locationManager.authorizationStatus
}
func requestWhenInUseAuthorization() {
locationManager.requestWhenInUseAuthorization()
}
func startUpdatingLocation() {
locationManager.startUpdatingLocation()
}
}
extension ViewModel: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
self.error = error
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
self.location = locations.last
if error != nil { self.error = nil }
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
self.authorizationStatus = manager.authorizationStatus
if authorizationStatus == .notDetermined {
requestWhenInUseAuthorization()
}
}
}