Checking iOS auth permissions before prompting or staring collection - ios

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.

Related

app only requesting location acces when in use

I want to use requestAlwaysAuthorization but for some reason it only gives the options to allow once, allow when in use, or dont allow. This is my code.
if CLLocationManager.locationServicesEnabled() {
switch locationManager.authorizationStatus {
case .notDetermined, .restricted, .denied:
locationManager.requestAlwaysAuthorization()
case .authorizedAlways:
break
case .authorizedWhenInUse:
locationManager.requestAlwaysAuthorization()
#unknown default:
break
}
} else {
locationManager.requestAlwaysAuthorization()
From Apple's documentation for requestAlwaysAuthorization:
You must call this or the requestWhenInUseAuthorization() method before your app can receive location information. To call this method, you must have both NSLocationAlwaysUsageDescription and NSLocationWhenInUseUsageDescription keys in your app’s Info.plist file. You may call requestAlwaysAuthorization() when the current authorization state is either:
Not Determined — CLAuthorizationStatus.notDetermined
When In Use — CLAuthorizationStatus.authorizedWhenInUse
Use the locationManager(_:didUpdateLocations:) method on the CLLocationManager delegate to receive updates when the user makes permission choices.
Core Location limits calls to requestAlwaysAuthorization(). After your app calls this method, further calls have no effect.
Based on this, few things could be wrong with your code.
Too many calls to requestAlwaysAuthorization. This should be sufficient:
if CLLocationManager.locationServicesEnabled() {
switch locationManager.authorizationStatus {
case .notDetermined, .authorizedWhenInUse:
locationManager.requestAlwaysAuthorization()
default:
print("Cannot ask user for requestAlwaysAuthorization")
}
}
When user has already denied location permission, asking for it again will not show the popup.
Missing NSLocationAlwaysUsageDescription and NSLocationWhenInUseUsageDescription in Info.plist. Define non-empty strings for these in your Info.plist.
Finally try reinstalling the app on device/simulator to clear previously granted or denied location permissions

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.

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.

How to detect system wide Location Services changes

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.

Resources