Deep Dive into iOS Location Permission

Yevhenii Smirnov
Yevhenii Smirnov
May 19 2023
Posted in Engineering & Technology

Location Permission request flow in iOS

Deep Dive into iOS Location Permission

We are back to our ongoing blog post series about permission requests. Check our previous blog post about iOS Notification permission request here.

In this post, we'll be delving into the intricacies of Location permission. We'll cover the recommended flow and discuss how to handle all possible scenarios to ensure a smooth and user-friendly experience.

Feel free to check out our previous articles on handling permission requests for Android, Flutter, and Cordova & Ionic.

Recommended Flow

In iOS we have two types of permissions you usually would like to request. With When in Use, the app will receive the user's location when the application is being used, and Always which means the user's location will be updated even when the user is not using the application.

Similar to Android, a permission upgrade should be done in steps, meaning requesting first When in Use and if granted upgrading to Always. Remember you should only request Always permission if your app provides the functionality need to justify that usage. When the application is using Always permission, the system may periodically alert the user of that usage, by providing a pop-up with an option to switch the permission to When in Use or keep the Always mode. If the user is not expecting the application to use location in the background this could lead to a poor user experience and eventually drive users to deny this type of permission.

It is worth mentioning that you have the option to request the Always permission directly. However, I strongly advise against doing so, as it can cause low opt-in rates. You can read more about this in the official documentation.

Now let's see the flow in steps:

  • Check the permission status
  • Based on the status:
    • If the status is Not Determined, it means the user has not yet made a decision regarding the permission. In this case, you should proceed with requesting When in Use. After When in Use is granted and if your application requires so, you may proceed to request the Always permission.
    • If the status is When in Use, it means you can already receive the user's location while the app is being uses. You may be able to request location Always permission.
    • If the status is Always, it means the application has permission to access the location all the time.
    • If the status is Denied, it indicates that the permission has been permanently denied by the user. In such a scenario, it is important to provide a clear explanation to the user about why the permission is required and direct them to the device's settings screen, where they can manually change the permission.
    • In case is Restricted, we may treat this case as permanently denied, but in this case, we should not redirect to the settings as the user cannot change this status. The application is not authorized to use location services due to active restrictions.

Important note: After When in Use permission is granted, you may or not be able to request the Always permission. That's because the user has 3 options: Allow the permission, Allow only once, or Don't Allow. When allowing only once you will not be able to request Always permission, and you cannot identify between the two options (Allow Once and When in Use). It's also important to note that when requesting the Always permission, the user has 2 options, grant the Always or keep When in Use. When the user keeps When in Use, the permission status will not be updated, and you have no way to act upon that response. I will go through this later in the permission request.

Basics of Location Permission

Before we start, your app must declare usage description texts explaining why it needs to track location. To do so, you need to include the appropriate keys and values in your app's Info.plist file. Make sure you provide a text that best describes why and how you are going to use the user's location. This text will be displayed to the user the first time you request the use of location services. Users are more likely to trust your app with location data if they understand why you need it.

<plist version="1.0">
<dict>
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>We will need to make use of your location to present relevant information about offers around you.</string>
    <key>NSLocationAlwaysUsageDescription</key>
    <string>We will need to make use of your location to present relevant information about offers around you.</string>
    <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
    <string>We will need to make use of your location to present relevant information about offers around you.</string>
</dict>
</plist>

To handle permissions, we will interact with the CLLocationManager:

private let locationManager = CLLocationManager()

And act accordingly on its status:

private func checkLocationPermissionStatus() {
    let status = CLLocationManager.authorizationStatus()
    switch status {
    case .authorizedWhenInUse:
        // Location When in Use is authorized
    case .authorizedAlways
        // Location Always is authorized
    case .denied:
        // Location permission is permanently denied
    case .restricted:
        // Location services are restricted
    case .notDetermined:
        // Location permission status has not been determined yet
    @unknown default:
        // Handle future permission cases if any
    }
}

Before proceeding with the request, it's important to note that, unlike notifications, where we can receive the response directly from the request, handling location permissions requires listening to authorization changes.

For that, we need to implement the CLLocationManagerDelegate:

override init() {
    super.init()
    locationManager.delegate = self
}

And wait for authorization changes:

extension YourViewController: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        switch status {
        case .authorizedWhenInUse:
            // Location When in Use is authorized
        case .authorizedAlways:
            // Location Always is authorized
        case .denied:
            // Location permission is permanently denied
        case .restricted:
            // Location services are restricted
        case .notDetermined:
            // Location permission status has not been determined yet
        @unknown default:
            // Handle future permission cases if any
        }
    }
}

Finally, we can use the following to request the permission:

private func requestWhenInUseLocationPermission() {
    locationManager.requestWhenInUseAuthorization()
}

private func requestAlwaysLocationPermission() {
    locationManager.requestAlwaysAuthorization()
}

This is a very basic setup that you can adapt to your needs.

Below, I will provide you with a more complex implementation example accompanied by comments to fully handle the location permission flow.

A Deeper Look

In the example provided below, I've outlined a flow that allows you to request both When in Use and Always permissions and enable location updates accordingly. You have the flexibility to modify the implementation to only request When in Use permission or incorporate a toggle button to enable or disable location updates based on your specific requirements. This adaptable approach ensures that you can customize the functionality to suit your application's needs.

import CoreLocation
import Foundation
import NotificareKit
import NotificareGeoKit

private let REQUESTED_LOCATION_ALWAYS_KEY = "re.notifica.geo.requested_location_always"

class YourViewController: NSObject {
    private let locationManager = CLLocationManager()
    private var requestedPermission: LocationPermissionGroup?
    private var authorizationStatus: CLAuthorizationStatus {
        if #available(iOS 14.0, *) {
            return locationManager.authorizationStatus
        } else {
            return CLLocationManager.authorizationStatus()
        }
    }

    override init() {
        super.init()
        locationManager.delegate = self
    }
}

extension YourViewController: CLLocationManagerDelegate {
    private func enableLocationUpdates() {
        if checkLocationPermissionStatus(permission: .locationAlways) == .granted {
            Notificare.shared.geo().enableLocationUpdates()

            return
        }

        // Checking location When in Use permission status
        let whenInUsePermission = checkLocationPermissionStatus(permission: .locationWhenInUse)

        switch whenInUsePermission {
        case .permanentlyDenied:
            // Location When in Use is permanently denied
            // Provide a clear explanation to the user about why the permission is required and direct them to the application settings screen, where they can manually enable the permission.
            return
        case .restricted:
            // Location Service is restricted
            // Due to active restrictions on location services, the user cannot change this status, and may not have personally denied authorization.
            return
        case .notDetermined:
            // Location When in Use is not determined, requesting permission
            requestLocationPermission(permission: .locationWhenInUse)
            return
        case .granted:
            // Location When in Use granted, enabling location updates
            Notificare.shared.geo().enableLocationUpdates()
        }

        // Checking location Always permission status
        let alwaysPermission = checkLocationPermissionStatus(permission: .locationAlways)

        switch alwaysPermission {
        case .permanentlyDenied, .restricted:
            // Location Always permission is permanently denied or restricted
            return
        case .notDetermined:
            // Location Always is not determined, requesting permission
            requestLocationPermission(permission: .locationAlways)
        case .granted:
            // Location Always permission is granted, enabling location updates
            Notificare.shared.geo().enableLocationUpdates()
        }
    }

    private func checkLocationPermissionStatus(permission: LocationPermissionGroup) -> LocationPermissionStatus {
        if permission == .locationAlways {
            switch authorizationStatus {
            case .notDetermined:
                return .notDetermined
            case .restricted:
                return .restricted
            case .denied:
                return .permanentlyDenied
            case .authorizedWhenInUse:
                return UserDefaults.standard.bool(forKey: REQUESTED_LOCATION_ALWAYS_KEY) ? .permanentlyDenied : .notDetermined
            case .authorizedAlways:
                return .granted
            @unknown default:
                return .notDetermined
            }
        }

        switch authorizationStatus {
        case .notDetermined:
            return .notDetermined
        case .restricted:
            return .restricted
        case .denied:
            return .permanentlyDenied
        case .authorizedWhenInUse, .authorizedAlways:
            return .granted
        @unknown default:
            return .notDetermined
        }
    }

    private func requestLocationPermission(permission: LocationPermissionGroup) {
        requestedPermission = permission

        if permission == .locationWhenInUse {
            locationManager.requestWhenInUseAuthorization()
        } else if permission == .locationAlways {
            locationManager.requestAlwaysAuthorization()

            // Helps us to identify if Always permission has already been requested
            UserDefaults.standard.set(true, forKey: REQUESTED_LOCATION_ALWAYS_KEY)
        }
    }

    internal func locationManager(_: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        onAuthorizationStatusChange(status)
    }

    @available(iOS 14.0, *)
    internal func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        onAuthorizationStatusChange(manager.authorizationStatus)
    }

    private func onAuthorizationStatusChange(_ authorizationStatus: CLAuthorizationStatus) {
        if authorizationStatus == .notDetermined {
            // When the user changes to "Ask Next Time" via the Settings app.
            UserDefaults.standard.removeObject(forKey: REQUESTED_LOCATION_ALWAYS_KEY)
        }

        guard let requestedPermission = requestedPermission else {
            // Location permission status did change but you didnt request any permission, meaning user did some changes in application settings
            return
        }

        self.requestedPermission = nil

        let status = checkLocationPermissionStatus(permission: requestedPermission)
        if requestedPermission == .locationWhenInUse {
            if status != .granted {
                // Location When in Use permission request denied

                return
            }

            enableLocationUpdates()
        }

        if requestedPermission == .locationAlways {
            if status == .granted {
                // Location Always permission request granted, enabling location updates

                Notificare.shared.geo().enableLocationUpdates()
            }
        }
    }
}

private extension YourViewController {
    enum LocationPermissionGroup: String, CaseIterable {
        case locationWhenInUse = "When in Use"
        case locationAlways = "Always"
    }

    enum LocationPermissionStatus: String, CaseIterable {
        case notDetermined = "Not Determined"
        case granted = "Granted"
        case restricted = "Restricted"
        case permanentlyDenied = "Permanently Denied"
    }
}

Let's go shortly through the example above:

  • We call enableLocationUpdates()
  • Check if Always permission is granted, when granted enable location updates, otherwise check if When is Use is granted:
    • If granted, enable location updates.
    • If denied (meaning permanently denied), we should forward the user to the application settings. Do not forget to provide a clear explanation to the user about why permission is required before redirecting.
    • If restricted, the user cannot change this status meaning your application could not request permission.
    • If not determined, we can proceed with the permission request.
  • We use CLLocationManagerDelegate to listen for any authorization change.
  • If When in Use is granted, we call enableLocationUpdates(), which enables location updates and proceeds to the Always permission request.
  • When requesting Always permission, we use UserDefaults to save that preference and will help us identify if the permission has already been requested.
  • When permission is granted, we enable location updates once again this time having continuous access to the user location updates.

Previously, I mentioned that when the user chooses to keep the When in Use permission during an Always request, you cannot directly listen to that change. If you want to handle this specific case, you would need to implement an observer to listen for when the permission popup is dismissed. This allows you to capture the user's decision and handle it accordingly in your application. Implementing an observer provides a way to handle the scenario where the user chooses to keep the When in Use permission instead of granting the Always permission.

Here is an example of how you can achieve that:

private func requestLocationPermission(permission: LocationPermissionGroup) {
    requestedPermission = permission

    if permission == .locationWhenInUse {
        locationManager.requestWhenInUseAuthorization()
    } else if permission == .locationAlways {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(applicationDidBecomeActive),
            name: UIApplication.didBecomeActiveNotification,
            object: nil
        )

        locationManager.requestAlwaysAuthorization()

        // Helps us to identify if Always permission has already been requested
        UserDefaults.standard.set(true, forKey: REQUESTED_LOCATION_ALWAYS_KEY)
    }
}

@objc private func applicationDidBecomeActive() {
    NotificationCenter.default.removeObserver(
        self,
        name: UIApplication.didBecomeActiveNotification,
        object: nil
    )

    if requestedPermission == nil {
        return
    }

    if (authorizationStatus != .authorizedAlways) {
        // Location Always permission denied (User opted to keep When in Use)
        requestedPermission = nil
    }
}

In case you want implement a toggle button, the method you invoke may look something like this:

func updateLocationServicesStatus(enabled: Bool) {
    // Location Toggle switched (enabled ? "ON" : "OFF")

    if enabled {
        enableLocationUpdates()
    } else {
        Logger.main.info("Disabling location updates")
        Notificare.shared.geo().disableLocationUpdates()
        checkLocationStatus()
    }
}

And the initial status for that toggle will usually be based on the location permission and Notificare.shared.geo().hasLocationServicesEnabled.

Finally, when redirecting the user to device's settings, don't forget to adapt the code to listen for changes and act accordingly.

Conclusion

In this blog post, we have delved into the intricacies of location permission requests, covering various scenarios and discussing effective strategies for handling them. By exploring the topic in detail, we aimed to provide you with a deeper understanding of the location permission flow and equip you with the necessary knowledge to handle different situations with confidence and precision.

As always, we hope this post was helpful and feel free to share with us your ideas or feedback via our Support Channel.

Keep up-to-date with the latest news