nurider
nurider

Reputation: 1833

Main thread warning with CLLocationManager.locationServicesEnabled()

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?

Upvotes: 48

Views: 44018

Answers (7)

Alex
Alex

Reputation: 159

func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        DispatchQueue.global().async {
            self.locationServicesEnabled = CLLocationManager.locationServicesEnabled()
            print(manager.authorizationStatus)
        }
    }

This approach ensures the locationServicesEnabled variable is only updated when it might actually change. Typically, you don't need to worry about this, and you can simply use locationManager.authorizationStatus. However, if you need to guide the user to the appropriate settings link, you may need to use the locationServicesEnabled method.

Upvotes: 0

Yohst
Yohst

Reputation: 1902

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
  }
}

Upvotes: 47

Alessio Toma
Alessio Toma

Reputation: 21

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()
}

Upvotes: 0

Arik Segal
Arik Segal

Reputation: 3031

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)
    }

Upvotes: 2

wh36
wh36

Reputation: 9

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
    }
}

Upvotes: -1

Edug
Edug

Reputation: 1

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

Upvotes: -1

Marcos Curvello
Marcos Curvello

Reputation: 917

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
    }
}

Upvotes: 8

Related Questions