0

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?

meaning-matters
  • 21,929
  • 10
  • 82
  • 142
  • AsyncStream is what you are looking for. – lorem ipsum Jan 31 '23 at 17:06
  • I've read [Apple's documentation](https://developer.apple.com/documentation/swift/asyncstream). I don't see how/why the single-shot authorization reply would fit using an `AsyncStream`. Would you be willing to elaborate or show it in code in an answer? – meaning-matters Jan 31 '23 at 17:41
  • 2
    It isn’t a single shot, that is what you are experiencing, it will call the delegate when you first call the request method and then when the user actually selects permission and if precision is changed it sometimes calls it a 3rd time. I dont have code to share, I have actually rewritten a similar locator object to be fully concurrent a half dozen times and because of the nature of the delegate it is quite difficult to get perfect. I have a “working” solution but it isn’t worthy of sharing, hopefully Apple provides a concurrent solution in June. – lorem ipsum Jan 31 '23 at 17:51
  • @loremipsum Thanks for sharing. At the moment I have a single UI view 'listening' to this `Locator`. Getting the logic/states correct of the UI using this class is quite tricky indeed. May I urge/ask to please share what you have, if you want; it doesn't have to be perfect and be helpful for me and others. – meaning-matters Jan 31 '23 at 18:30
  • You are correct, an AsyncStream won't help here. An async stream works where you have a single caller expecting multiple results. Here you have multiple callers, each expecting a single result. All you need to do is change your `authorizationContinuation` property to an array. Append new continuations to this array and pop the first element in the delegate method and resume it. – Paulw11 Jan 31 '23 at 19:56

1 Answers1

1

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()
        }
    }
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044