How to force 'always' location access in iOS app

Viewed 3423

The app that I'm building needs always location access to work properly. The app basically tracks location and puts it on a map and stuff (details not important, lol).

My goal is this:

  1. Prompt user to enable "always" location access
  2. If always location access has been requested but the user said no, make the app unusable - basically just show a little button that redirects them to settings where they can change that setting.

My AppDelegate.swift is implementing CLLocationManagerDelegate, and the code is as follows:

alreadyRequestedLocationWhenInUse = UserDefaults.standard.bool(forKey: "alreadyRequestedLocationWhenInUse")
alreadyRequestedLocationAlways = UserDefaults.standard.bool(forKey: "alreadyRequestedLocationAlways")

        func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
      
        switch CLLocationManager.authorizationStatus() {
        case .notDetermined:
            if (!alreadyRequestedLocationWhenInUse) {
                print("Requesting location access 'while in use'.")
                self.locationManager.requestWhenInUseAuthorization();
                UserDefaults.standard.set(true, forKey: "alreadyRequestedLocationWhenInUse")
                alreadyRequestedLocationWhenInUse = true
            } else {
                promptToChangeLocationSettings()
            }
        case .restricted, .denied:
            print("No Location access")
            promptToChangeLocationSettings()
            break;
        case .authorizedWhenInUse:
            if (!alreadyRequestedLocationAlways) {
                print("Requesting location access 'Always'.")
                self.locationManager.requestAlwaysAuthorization()
                UserDefaults.standard.set(true, forKey: "alreadyRequestedLocationAlways")
                alreadyRequestedLocationAlways = true
            } else {
                promptToChangeLocationSettings()
            }
            break;
        case .authorizedAlways:
            self.startLocationMonitoring();
            break;
        default:
            self.locationManager.requestAlwaysAuthorization();
            return
        }
    }

where promptToChangeLocationSettings() is a properly working function that takes the user to the settings page for my app.

The problem is that the user isn't prompted to enable "Always location tracking" until they exit the app and come back. They are asked for 'while in use' permissions (and I know that the way it works is they have to say yes to that first), but I want the always prompt to happen right away! In theory, the locationManagerDidChangeAuthorization function should be called AGAIN after the 'while use' authorization is granted, but this does not happen! Why does this not happen? Instead, promptUserToChangeLocationSettings() runs and makes the app unusable BEFORE the user gets the little poup that asks whether they want to enable 'always' location access.

Can someone help me fix this? By the way, I am using UserDefaults to keep track of whether we have done a location permission request (as the request can only be done once).

2 Answers

A few observations about this flow where we request “when in use” first, and when that's granted, only then request “always” (as discussed in WWDC 2020 What's New in Location):

  1. Make sure you run this on a device, not the simulator. You may not see the subsequent “upgrade ‘when-in-use’ to ‘always’” permission alert when using a simulator.

  2. This feature was introduced in iOS 13.4. Make sure you are not attempting this on an earlier iOS 13 version. On those earlier versions, you won’t see the second alert to upgrade to “always”.

  3. Make sure you don’t have a lingering requestAlwaysAuthorization elsewhere in your code-base that might have put the app in a “provisional always” state. Once in provisional state, you are locked into the provisional flow of 13.0.


I know it isn’t what you’re looking for, but for the sake of future readers, the alternative to the above is the simpler “provisional always” flow introduced in iOS 13.0 (outlined in WWDC 2019's What's New in Core Location). You just call requestAlwaysAuthorization (never calling requestWhenInUseAuthorization). Apple's intent here was to let the user better reason about what’s going on, showing the “when in use” alert while the app is in use and automatically showing the “always” upgrade alert when location services are used while the app isn't running.

This is a solution that got the result I desired: Firstly: Make a call to locationManagerDidChangeAuthorization(locationManager) in the AppDelegate.swift didFinishLaunchingWithOptions function. I also called it in applicationWillEnterForeground just so that it rechecks every time the app opens.

Secondly, this is my new locationManagerDidChangeAuthorization function. Just need to remove the return/break statements, but I'm just gonna answer this now before I forget:

func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
  
    switch CLLocationManager.authorizationStatus() {
    case .notDetermined:
        UserDefaults.standard.set(false, forKey: "alreadyRequestedAlwaysLocationAccess")
        alreadyRequestedAlwaysLocationAccess = false
        DispatchQueue.main.async{
            self.coreLocationManager.requestWhenInUseAuthorization()
            self.locationManagerDidChangeAuthorization(manager)
        }
        break;
    case .restricted, .denied:
        print("No Location access")
        promptToChangeLocationSettings()
        break;
    case .authorizedWhenInUse:
        if (!alreadyRequestedAlwaysLocationAccess) {
            print("Requesting location access 'Always'.")
            UserDefaults.standard.set(true, forKey: "alreadyRequestedAlwaysLocationAccess")
            alreadyRequestedAlwaysLocationAccess = true

            DispatchQueue.main.async{
                self.coreLocationManager.requestAlwaysAuthorization()
                self.locationManagerDidChangeAuthorization(manager)
            }
        } else {
            promptToChangeLocationSettings()
        }
        break;
    case .authorizedAlways:
        self.startLocationMonitoring();
        break;
    default:
        return
    }
}
Related