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.
Related
As per the quick read the purpose of this key
"PHPhotoLibraryPreventAutomaticLimitedAccessAlert" is to prevent limited library access. I have added this option in info.plist but it still shows "Select photos" option in permissions dialogue.
The code of getting permissions is following.
func checkPhotoLibraryPermission(completionBlock completion: #escaping ()->Void) {
let status = PHPhotoLibrary.authorizationStatus()
switch status {
case .authorized:
completion()
break
case .notDetermined,.denied,.restricted:
// ask for permissions
PHPhotoLibrary.requestAuthorization { (status) in
switch status {
case .authorized:
completion()
break
case .notDetermined,.denied,.restricted:
self.alertToEncouragePhotoLibraryAccessWhenApplicationStarts()
break
case .limited:
break
}
}
break
case .limited:
break
}
}
Anyone please mention what is the exact purpose of this key and is it possible to even force drop this option ?
As far as i know the purpose of this key "PHPhotoLibraryPreventAutomaticLimitedAccessAlert" is to prevent limited library access.
Then you "know" wrong. You cannot prevent use of limited authorization. It is built into the system and the user can always specify it.
So what is this key for? Well, if the user does specify limited access, the system may put up the Select Photos interface again from time to time to see whether the user wants to change what photos your app can access. PHPhotoLibraryPreventAutomaticLimitedAccessAlert prevents that. It does not prevent the user from specifying limited access in the initial authorization request alert or the Settings 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
}
}
If I deny at first, then go to settings and allow in settings, in both cases the status is notDetermined, instead of denied then authorized.
Why is that happening?
It doesn't save the image when i click "Don't allow", but status becomes .notDetermined not .denied .
It saves, after i go to settings->Photos, uncheck "Never" and check "Add Photos Only". But the status stays .notDetermined, does not become .authorized
func save(){
guard let image = imageView.image else {return}
UIImageWriteToSavedPhotosAlbum(image, self, nil, nil)
let status = PHPhotoLibrary.authorizationStatus()
switch status {
case .authorized:
print("authorized")
return
case .notDetermined:
print("not determined")
case .denied, .restricted:
print("denied or restricted")
//please go to settings and allow access
promptToSettings()
}
}
I am asking permission to save an image to photo library.
When the first time the user tries to save, he gets asked: "App would like to add to Photos" "Don't Allow" "Ok"
If the user denied then tried to save again,i want to check and if the status is .denied, prompt the user to go to settings and allow.
But the code goes to .notDetermined block when the user does not give access the first time. It stays .notDetermined even after in settings the user allows access.
I downloaded your code and ran it. I was able to experience whatever you said. It always returned Not Determined status.
I did a little bit analysis on your code further. Please find my observation below.
In your current code, "PHPhotoLibrary.requestAuthorization" method is not called before reading the authorization status using "PHPhotoLibrary.authorizationStatus" method.
Though "UIImageWriteToSavedPhotosAlbum" in Save method triggers a Photos app permission pop up, it does not help here completely.
So, I called "askForAccessAgain" method in ViewDidLoad method in order to get the permission using "PHPhotoLibrary.requestAuthorization" method first.
After that, whenever i saved the photos using Save method, it returned the correct status, let it be either "Authorized" or "Denied".
If I choose "Never" and "Allow Photos Only", it returned "Denied or Restricted" status. When "Read and Write" is chosen, "authorized" is returned.
So, it looks like, We need to call "PHPhotoLibrary.requestAuthorization" method to get the permission first instead of relying on other Photo access methods ( like UIImageWriteToSavedPhotosAlbum) for getting the permission. Only then, correct status is returned by "PHPhotoLibrary.authorizationStatus".
One more addition info:
App Permissions are retained even after deleting the app and reinstalling it. Just take a look on this as well. It might help you in troubleshooting similar issues in future.
iPad remembering camera permissions after delete—how to clear?
Please let me know if this has resolved your issues.
I need to access users Photos, for this I ask for permission. There are possible outcomes :
Permission granted : In this condition application resumes it normal functioning.
Permission Denied : In this case application shows an alert wherein user has option to goToSettings or to just ignore it. If user selects goToSettings and provides access to photos and comes back to application then it still says that permission is not granted. Permissions are only refreshed when I restart my application.
My Question: How to refresh the application's permission settings without restarting my application. Similar problem to my question is posted here.
Edit:
My application is a navigation controller based application and I check for access in my controller's viewDidLoad. I just tried if request permissions are refreshed if I pop and push the controller again. No luck, the results where same - it still said permission denied.
Edit 2 : Code to check for permissions
func requestAccess() -> Void {
PHPhotoLibrary.requestAuthorization {
status in
switch(status) {
case .notDetermined, .denied :
// perform relavent action
break
case .authorized , .restricted :
// perform relavent action
break
}
}
}
iOS kills your app when you changed privacy settings. And after next tap on app's icon your app is started from scratch. application:didFinishLaunching is called and so on. So you don't need to update permissions.
In your AppDelegate you could have logic in applicationDidBecomeActive to re-query the permission store for your updated permissions.
In your viewDidLoad for the view you check permissions on you could observe the notification like so;
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: #selector(yourClass.applicationDidBecomeActive(_:)),
name: UIApplicationDidBecomeActiveNotification,
object: nil)
The code below is Objective-C.
I think you can do this when user tapping a button or something:
PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
if (status == PHAuthorizationStatusNotDetermined) {
// ask for authorization
[PHPhotoLibrary requestAuthorization...
} else if (status == PHAuthorizationStatusAuthorized) {
// continue accessing photos
} else {
// access denied, go to settings if you want
}
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.