How to detect system wide Location Services changes - ios

I'm going through potential cases where a User might turn off Location Services on their phone.
Right now, I have:
func locationManager(manager: CLLocationManager!, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
if status == .Authorized || status == .AuthorizedWhenInUse {
manager.startUpdatingLocation()
} else {
manager.stopUpdatingLocation()
}
}
I assume this will trigger whenever the User goes into the Settings and changes my App's settings personally. Does this also trigger whenever the user turns off Location Services for all applications? The documentation doesn't seem to go over it.

Yes. Turning of location services globally will also trigger that method and it effectively means that the kCLAuthorizationStatusDenied will be sent as the current status.
Once the user enables the location services again, the method will be triggered again and one of the other enumerated options will be sent as the current status - And the value will depend on the location services settings history for that specific app.

Related

How to force 'always' location access in iOS app

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:
Prompt user to enable "always" location access
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).
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):
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.
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”.
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
}
}

What makes provisional Always authorization provisional?

According to Apple, if you ask for your Core Location app to get Always authorization when the authorization is "not determined", the user sees the dialog for When In Use authorization but in fact your app gets Always authorization — provisionally.
This is supposed to mean that if you don't actually use your Always powers, you will lose them, reverting to When In Use.
Okay, but when will that reversion happen? I can't seem to make it happen. My app just stays at Always authorization, even though the user thinks it is only When In Use authorization.
Here's the entire code of my test app (iOS 14):
class ViewController: UIViewController, CLLocationManagerDelegate {
#IBOutlet weak var label: UILabel!
let locman = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
locman.delegate = self
}
#IBAction func doAskForAlways(_ sender: Any) {
self.checkForLocationAccess(always:true)
}
func checkForLocationAccess(always:Bool = false, andThen f: (()->())? = nil) {
let status = self.locman.authorizationStatus()
switch status {
case .authorizedWhenInUse:
if always { // try to step up
self.locman.requestAlwaysAuthorization()
} else {
f?()
}
case .authorizedAlways:
f?()
case .notDetermined:
if always {
self.locman.requestAlwaysAuthorization()
} else {
self.locman.requestWhenInUseAuthorization()
}
case .restricted:
break
case .denied:
break
default: fatalError()
}
}
fileprivate func updateStatus(_ status: CLAuthorizationStatus) {
self.label.text = {
switch status {
case .authorizedAlways: return "Always"
case .authorizedWhenInUse: return "When In Use"
default: return ""
}
}()
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
let status = manager.authorizationStatus()
print("authorization is", status.rawValue)
updateStatus(status)
}
#IBAction func doStatus(_ sender: Any) {
self.updateStatus(self.locman.authorizationStatus())
}
}
You need two buttons and a label. Tap the first button to ask for Always authorization when you have no authorization to start with ("not determined"). You see the When In Use authorization dialog. Grant authorization. Now play with the app and keep watching the status display in the label. You can tap the second button to update the status if needed.
The problem is that it stays at Always. When will my "provision" come to an end so that the authorization reverts to When In Use? How can I encourage this to happen?
In WWDC 2019's What's New in Core Location, they outline the basic process in iOS 13.0:
Your app requests “always” permission.
The user sees “when in use” permissions alert, not an “always” permission alert:
If the user grants “when in use” the app is in “provisional always” state.
In this case, and somewhat confusingly, the authorizationStatus will return .authorizedAlways when you are in this “provisional always” state and the Settings app on the phone will suggest it’s in “when in use” state. But in reality, it’s in this “provisional always” state, not quite what one might infer from authorizationStatus nor from what you see in the Settings app.
Needless to say, if the user doesn't even grant “when in use” (e.g. they deny or chose “only once”), then obviously you won’t be in “provisional always” state.
It remains in this provisional state until, as the video says, you “start using ‘always’ powers”. For example, if you start significant change service and move a distance sufficient to trigger a significant change.
When the app does “start using ‘always’ powers”, the OS will ask the user if they are is willing to upgrade “when in use” to “always”. (It won't always happen immediately, but will wait until the user is not busy doing other things, to reduce the risk that they'll dismiss the alert just to get back to what they were doing.)
So, it’s not a question of “reverting” to some other state. The app will remain in this “provisional always” state until there is final “agreement” (where the user sees the second alert and either agrees to upgrade to .authorizedAlways or denies and it is set to .authorizedWhenInUse).
I know you know this, but for the sake of future readers:
In WWDC 2020 video What's new in location, they describe a change introduced in iOS 13.4. Instead of the flow above (where you ask for “always”, the user sees “when in use” permissions, and they don’t see the “upgrade to always” until “always” services are actually triggered), iOS 13.4 introduced a new flow, where you can ask for “when in use” (rather than “always”) and assuming the user granted it, you can ask for “always” later, where appropriate in the app, and the user get the second alert (this time asking if the user would like to upgrade to “always” or not). You just need the appropriate permissions strings.

Checking iOS auth permissions before prompting or staring collection

I'm unclear as to Apple's intent for starting location monitoring. Say I want to request "Always" auth, and get the location callbacks enabled by startUpdatingLocation(). Apple docs for requestAlwaysAuthorization() say:
You may call requestAlwaysAuthorization() when the current authorization state is either .notDetermined or .authorizedWhenInUse. ... Your app can no longer ask for Always authorization after the user responds to this prompt. Calling this method again will have no effect.
This implies we should check the state before calling it. However the docs on authorizationStatus() say:
Use of authorizationStatus() is unnecessary and discouraged. Instead, implement the locationManager(_:didChangeAuthorization:) delegate callback to receive up-to-date authorization status.
So should you just always call requestAlwaysAuthorization() when you start up even if you already have permissions, and then call startUpdatingLocation() if you get an "authorized" status in the didChangeAuthorization callback? Is the callback guaranteed to occur? Or should you just call startUpdatingLocation() whether you've gotten an authorized callback or not, and you'll start getting updates when the permissions allow it? The last bit is what I've seen in testing, but I don't want to run afoul of Apple's guidelines on this.
You should call requestAlwaysAuthorization() when you start up like this -
override func viewDidLoad() {
super.viewDidLoad()
locationManager = CLLocationManager()
locationManager?.delegate = self
locationManager?.requestAlwaysAuthorization()
}
and then call startUpdatingLocation() once you get the result
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedAlways {
// start updating location here
}
}
The didChangeAuthorization is always called and if the user has already given the permission the code will execute normally otherwise when user gives the permission the code will execute after matching the condition.

Location permission issue iOS 11 and iOS 10

I am having an issue when requesting location permissions from user when I use iOS11 my info.plist contains
<key>NSLocationWhenInUseUsageDescription</key>
<string>When in use permissions</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>always permissions</string>
<key>NSLocationAlwaysAndWhenInUsageDescription</key>
<string>Always and in usage permissions</string>
I have two maps one for customer and another for employees. For employees I need to know their location even if the app is not running or is backgrounded (they are able to turn it off when signing out) and request permission using
locationManager.requestAlwaysAuthorization()
For customer i only need locations while the app is in use and request the permission using
locationManager.requestWhenInUseAuthorization()
In iOS 11 this only requests permission when in usage and never the always on permission.
In iOS 10 it has the correct behaviour.
The behaviour I want is as follows:
When they are a customer (not signed in) it only asks for when in use permission. If they sign in (employee) it request location even when not in use.
If anyone can shed some light on what I am missing / done wrong it would be much appreciated.
Something to note if i remove the permission NSLocationAlwaysUsageDescription iOS10 and iOS11 have the same issue of not requesting always permission.
A bit more clarification.
I have implemented didChangeAuthorization delegate function and it gets called when a users allow the permission from alert from calling requestWhenInUseAuthorization()
however when I call requestWhenInUseAuthorization() function on location manager the delegate method is not called it is like it's never receiving that call and no alert dialog is shown to the user.
I figured out the issue by creating a quick stand alone app that only asked for permissions, I was given an error log that stated the keys I was using were wrong.
I had NSLocationAlwaysAndWhenInUsageDescription instead of NSLocationAlwaysAndWhenInUseUsageDescription which is odd because from the docs it states that NSLocationAlwaysAndWhenInUsageDescription should be used. Switching to include the correct key fixed issue and now permissions works as expected for iOS 11 and 10.
Thanks for all the help.
In your info.plist file add this:
<key>NSLocationUsageDescription</key>
<string></string>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
Now in your swift file, don't forget to add delegate: CLLocationManagerDelegate
In your viewDiDLoad(), add this:
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
locationManager.startMonitoringSignificantLocationChanges()
// Here you can check whether you have allowed the permission or not.
if CLLocationManager.locationServicesEnabled()
{
switch(CLLocationManager.authorizationStatus())
{
case .authorizedAlways, .authorizedWhenInUse:
print("Authorize.")
break
case .notDetermined:
print("Not determined.")
break
case .restricted:
print("Restricted.")
break
case .denied:
print("Denied.")
}
}
For both cases, customers and employees, you first need to call locationManager.requestWhenInUseAuthorization()
Then, only if they are employees, add a call to
locationManager.requestAlwaysAuthorization()
See https://developer.apple.com/documentation/corelocation/choosing_the_authorization_level_for_location_services/request_always_authorization
Overview To configure always authorization for location services, do
the following: Add the NSLocationWhenInUseUsageDescription key and the
NSLocationAlwaysAndWhenInUsageDescription key to your Info.plist file.
(Xcode displays these keys as "Privacy - Location When In Use Usage
Description" and "Privacy - Location Always and When In Use Usage
Description" in the Info.plist editor.) If your app supports iOS 10
and earlier, add the NSLocationAlwaysUsageDescription key to your
Info.plist file. (Xcode displays this key as "Privacy - Location
Always Usage Description" in the Info.plist editor.) Create and
configure your CLLocationManager object. Call the
requestWhenInUseAuthorization() initially to enable your app's basic
location support. Call the requestAlwaysAuthorization() method only
when you use services that require that level of authorization.
** Latest Working code in Swift 5.1:**
Plist: Add the entry
<key>NSLocationWhenInUseUsageDescription</key>
<string>Needs Location when in use</string>
import UIKit
import CoreLocation
class ViewController: UIViewController {
var locationManager: CLLocationManager?
override func viewDidLoad() {
super.viewDidLoad()
locationManager = CLLocationManager()
//Make sure to set the delegate, to get the call back when the user taps Allow option
locationManager?.delegate = self
}
}
extension ViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .notDetermined:
print("not determined - hence ask for Permission")
manager.requestWhenInUseAuthorization()
case .restricted, .denied:
print("permission denied")
case .authorizedAlways, .authorizedWhenInUse:
print("Apple delegate gives the call back here once user taps Allow option, Make sure delegate is set to self")
}
}
}

Handling Built In iOS Permission Dialogs

When loading my view controller for the first time, the user is prompted with a built in iOS permissions message: "Allow "appName" to access your location while you use the app?"
is there a way I can pause the app until the user either selects Don't Allow or Allow?
After the user selects Allow Or Dont Allow, I can handle it by checking the value like so:
//INSIDE OF ViewDidAppear
if(authstate != CLAuthorizationStatus.Denied)
{
// do something
}
else if (authstate == CLAuthorizationStatus.AuthorizedWhenInUse)
{
// do something
}
Right now the code steps through the if statement even though the iOS permissions dialog is still showing on the screen. Therefore, I want to pause it until the user selects Allow or Dont Allow. Then after the user selects either Allow or Dont Allow, continue onto the if statement
no. the permission request happens asynchronously. you have to implement the CLLocationManagerDelegates method
func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
// check the status
}
that gets called after the user answered the permission dialogue.

Resources