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 requestingWhen in Use
. AfterWhen in Use
is granted and if your application requires so, you may proceed to request theAlways
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 locationAlways
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.
- If the status is
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 ifWhen 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 callenableLocationUpdates()
, which enables location updates and proceeds to theAlways
permission request. - When requesting
Always
permission, we useUserDefaults
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.