Main thread warning with CLLocationManager.locationServicesEnabled()
Asked Answered
C

6

40

I just upgraded to Xcode 14.0 and when I run our app on iOS 16 devices, calls to:

CLLocationManager.locationServicesEnabled()

Are returning the warning:

This method can cause UI unresponsiveness if invoked on the main thread. Instead, consider waiting for the -locationManagerDidChangeAuthorization: callback and checking authorizationStatus first.

I'd need to make significant changes to my code if I have to wait for a failure/callback rather than just calling the CLLocationManager.locationServicesEnabled() method directly. This only seems to happen on iOS 16 devices. Any suggests on how to address this?

Chill answered 21/9, 2022 at 18:9 Comment(6)
don't you ask for authorization if you don't have it? and if you do, that would be the same code as apple wants you to implement here (i.e. regardless of current authorization status, ask for it, and process an async response)Enclasp
@khjfquantumjj You know that authorizationStatus and locationServicesEnabled() are returning two entirely distinct statuses, right?Spectrohelioscope
@Spectrohelioscope read the question. Based on warning the OP receives, it could be that he is trying to get locationServicesEnabled when user didn't authorize the access to location services, while authorization is a prerequisite to be able to obtain locationServicesEnabled status.Enclasp
None of the suggested solutions worked for me. I had to reconfigure my code to move this call off the main thread. I couldn't find another way around it.Chill
@akjndklskver @Spectrohelioscope You can check authorizationStatus on a CLLocationManager instance without having permission. If the user has not granted permission, then the status will be denied.Grimsley
@bugloaf You're wrong: The status might be denied because user disabled only for that app: 1) If you disable location services: status = denied, locationServicesEnabled = false 2) If you disable location services only for your app: status = denied, locationServicesEnabled = true 3) If you enable for both of them: status = always/while using.., locationServicesEnabled = trueQuestor
I
41

Use a dispatch queue to move it off the main queue like so:

DispatchQueue.global().async {
  if CLLocationManager.locationServicesEnabled() {
    // your code here
  }
}

EDIT 5/2/24 The above solves the immediate problem reported by the OP. However, as pointed out in the comments, may lead one to accept that anything may go on the global queue. A more refined solution, therefore, is to use a custom queue managed by your application such as:

let myQueue = DispatchQueue(label:"myOwnQueue")
myQueue.async {
  if CLLocationManager.locationServicesEnabled() {
    // your code here
  }
}
Irrupt answered 3/11, 2022 at 20:20 Comment(2)
Two other options are to wrap in Task.detached or use async let enabled = CLLocationManager.locationServicesEnabled() when await enabled within a Task. Any comments which is preferred?Hagiocracy
One of the worst things you could do is arbitrarily use global queues which cause thread explosions. On a multitude of occasions, Apple engineers have directly noted this is a poor practice and to create your own queues -- I believe in 2016/17 WWDC videos this was directly targeted ontop of an article from a former Apple engineer labeled the worst practices they saw.Sower
E
9

I faced the same issue and overcame it using Async/Await.

Wrap the CLLocationManager call in an async function.

func locationServicesEnabled() async -> Bool {
    CLLocationManager.locationServicesEnabled()
}

Then update the places where you use this function accordingly.

Task { [weak self] in

    if await self?.locationServicesEnabled() { 
        // Do something
    }
}
Eboh answered 4/10, 2022 at 14:24 Comment(4)
This still gives me the same warning.Berga
This method can cause UI unresponsiveness if invoked on the main thread. Instead, consider waiting for the -locationManagerDidChangeAuthorization: callback and checking authorizationStatus first.Brentwood
Yes, the example code above should consider that Tasks inherit the context in which they're instantiated.Eboh
Putting the CLLocationManager.locationServicesEnabled() call in an async func doesn't actually make it asynchronous since there isn't anything to await.Larisa
T
1

Instead of calling locationServicesEnabled directly, wrap it in a thread safe manner:

extension CLLocationManager {
func locationServicesEnabledThreadSafe(completion: @escaping (Bool) -> Void) {
    DispatchQueue.global().async {
         let result = CLLocationManager.locationServicesEnabled()
         DispatchQueue.main.async {
            completion(result)
         }
      }
   }
}

Usage example:

    CLLocationManager().locationServicesEnabledThreadSafe { [weak self] areEnabled in
        guard let self = self else {
            return assertionFailure()
        }
        self.handleLocationServices(enabled: areEnabled)
    }
Tabes answered 22/1 at 9:32 Comment(1)
One of the worst things you could do is arbitrarily use global queues which cause thread explosions. On a multitude of occasions, Apple engineers have directly noted this is a poor practice and to create your own queues -- I believe in 2016/17 WWDC videos this was directly targeted ontop of an article from a former Apple engineer labeled the worst practices they saw.Sower
R
0

If you must to return a value for example from func didTapMyLocationButton(for mapView: GMSMapView) -> Bool you can run it in the DispatchQueue.global() and await for execution with a DispatchSemaphore like this:

private func hasLocationPermission() -> Bool {
    var hasPermission = false
    let manager = CLLocationManager()
    let semaphore = DispatchSemaphore(value: 0)
    
    DispatchQueue.global().async {
        //This method can cause UI unresponsiveness if invoked on the main thread
        if CLLocationManager.locationServicesEnabled() {
            switch manager.authorizationStatus {
            case .notDetermined, .restricted, .denied:
                hasPermission = false
            case .authorizedAlways, .authorizedWhenInUse:
                hasPermission = true
            @unknown default:
                break
            }
        } else {
            hasPermission = false
        }
        semaphore.signal()
    }
    semaphore.wait()
    return hasPermission
}

then

  func didTapMyLocationButton(for mapView: GMSMapView) -> Bool {
    return hasLocationPermission()
}
Retharethink answered 7/3 at 10:11 Comment(0)
P
-1

In my case I had to separate both main.async and global().async to check some options in the background.

Pitfall answered 27/7, 2023 at 11:40 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Loyalist
C
-1

I'm quite new to Swift but with iOS 17 i've found this to work instead of checking locationServicesEnabled() you check the authorizationStatus:

func checkLocationServicesEnabled() {
    self.locationManager = CLLocationManager() // initialise location manager if location services is enabled
    self.locationManager!.delegate = self // force unwrap since created location manager on line above so not much of a way this can go wrong
    self.locationManager?.requestAlwaysAuthorization()
    self.locationManager?.desiredAccuracy = kCLLocationAccuracyBest
    
    switch self.locationManager?.authorizationStatus { // check authorizationStatus instead of locationServicesEnabled()
        case .notDetermined, .authorizedWhenInUse:
            self.locationManager?.requestAlwaysAuthorization()
        case .restricted, .denied:
            print("ALERT: no location services access")
    case .authorizedAlways:
        break
    case .none, .some(_):
        break
    }
}
Chaeta answered 13/1 at 23:1 Comment(2)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Loyalist
Checking locationServicesEnabled() checks if location services are enabled at the device level. Testing .authorizationStatus enumerates the users selection for your app's specific authorization level. In practices, one needs to check both locationServicesEnabled and .authorizationStatus. However, the OP asked for the first not the latter and thus this answer does not solve the OP's problem nor can a check against authorizationStates be used as an equivalent/replacement of locationServicesEnabled as suggested by @ChaetaIrrupt

© 2022 - 2024 — McMap. All rights reserved.