Permission alert to access user's address book does not halt execution - ios

I have a button to import contacts from the user's address book. When they tap the button for the first time, my code returns a bool indicating whether user has granted permission or not. If permission is granted, then the contacts are imported and I perform a segue to a table view. If permission is not granted then the contacts are not imported and I perform the same segue to an empty table view. Here's what I have:
func myImportThenSegueFunction() {
if userHasAuthorizedAddressBookAccess() == true {
// Import contacts and perform segue
} else {
// Just perform segue
}
}
func userHasAuthorizedAddressBookAccess() -> Bool {
switch ABAddressBookGetAuthorizationStatus() {
case .Authorized:
return true
case .NotDetermined:
var userDidAuthorize: Bool!
ABAddressBookRequestAccessWithCompletion(nil) { (granted: Bool, error: CFError!) in
if granted {
userDidAuthorize = true
} else {
userDidAuthorize = false
}
}
return userDidAuthorize
case .Restricted:
return false
case .Denied:
return false
}
}
The problem is that when the button is tapped, the permission to access settings alert is briefly displayed while in the background the view performs the segue before the user has either granted or denied access in the alert prompt. Then the app immediately crashes saying that .NotDetermined returned nil meaning the variable userDidAuthorize returned before the user could even make a selection to set userDidAuthorize in the if/else block in ABAddressBookRequestAccessWithCompletion(nil).
My question is why does the code execute return in the case .NotDetermined before the user sets the variable userDidAuthorize to return the proper bool via the alert prompt? I've seen some hacks to wrap the execution in a time delay and I know this functionality is deprecated. How would I get this to work properly?

Because ABAddressBookRequestAccessWithCompletion is asynchronous and returns immediately. It needs to be like this as it's usually used from the main thread and can't block while the user is asked for permission.
You need to change the way you use it to embrace this, by calling a completion block with the status once you know it rather than returning the status immediately.

Related

How do you redirect user to settings after denying permissions for Save Image from UIActivityController?

After a user takes a photo in my app, there is a UIUserActivity that is presented that allows users to save and image. A pop up comes up and asks for permission to write the photo to the users photo album. However, if the user denies there is no way to prompt the user to be redirected to the photo library. PHPhoto authorization status is always returning undetermined even though permissions were already asked for. I am looking for a few things to be satisfied:
the user should be able to deny but continue to be prompted to give permission to the app if they want to save their photo.
if the user denies, I want the option of "save photo" to remain in the UIUserActivity as a potential option
How can I accomplish this?
I've tried using the PHPhotoLibrary authorization status, but it always returns undetermined. I've tried checking for .undetermined and using the PHPhotoLibrary to request access to the user's camera roll, however if the user denies at this point then the option to save photo is completely removed from the UIUserActivity pop up.
code:
activityViewController.completionWithItemsHandler = { activity, success, items, error in
if success {
if let activity = activity {
...
case .saveToCameraRoll:
handleCameraRollPermission()
func handleCameraRollPermission(status: PHAuthorizationStatus? = nil, completion: #escaping ((Bool) -> Void)) {
let authorizationStatus = status ?? PHPhotoLibrary.authorizationStatus()
switch authorizationStatus {
case .denied, .restricted:
showPermissionMissingAlert(completion: completion)
case .notDetermined:
PHPhotoLibrary.requestAuthorization { (status) in
switch(status) {
case .denied, .restricted, .notDetermined:
self.showPermissionMissingAlert(completion: completion)
default:
break
}
}
default:
completion(true)
}
}
This might be a bit overdoing it, but you could create a custom action section for the activityView. You can find more about that here and scroll down to the Adding a custom action section.
Basically you would make a custom save button that would persist regardless of permission status. When it is tapped you can check their authorization status and go from there.

How to know if locationManager.requestAlwaysAuthorization() has already been asked

When requesting user's iOS location permissions, how could I know if locationManager.requestAlwaysAuthorization() has already been asked to the user?
In case the user had .AuthorizedWhenInUse status and the request for always authorization has been denied, the always-auth prompt for the next request won't be shown so I won't get any callback of this request launch.
Any ideas?
You need to check CLLocationManager.authorizationStatus() and only request authorization if the value is .notDetermined, since this is the only case when the authorization prompt will actually be shown.
You can check the Authorization status and compare if it's notDetermined it has not been asked, else - it's been asked.
You can know by using authorizationStatus() like this.
if CLLocationManager.authorizationStatus() == .notDetermined {
print("Not Determined")
}
else if CLLocationManager.authorizationStatus() == .restricted {
print("Restricted")
}
else if CLLocationManager.authorizationStatus() == .denied {
print("Denied")
}
else if CLLocationManager.authorizationStatus() == .authorizedAlways {
print("Always Authorized")
}
else if CLLocationManager.authorizationStatus() == .authorizedWhenInUse {
print("Authorized When Require")
}
If the Dialog appear for 1st time it returns .notDetermined status and if you respond to dialog than it returns status based on your selection like if you allow to access your location always that it returns .authorizedAlways.
After a lot of brainstorming this issue, I've found a solution.
Check authorization status each time the app starts, or when the app enters the foreground after the user views the Settings app.
If status is notDetermined, request authorizedWhenInUse. You can't jump straight to authorizedAlways.
If status is authorizedWhenInUse, request authorizedAlways.
The catch, as you know, is when the user goes from notDetermined to notDetermined, i.e. doesn't change their settings. We need to be able to see what the status was before they were prompted.
This is actually easy, just save the value of CLLocationManager.authorizationStatus() in a property called previousAuthStatus.
private var previousAuthStatus = CLLocationManager.authorizationStatus()
When the application enters the foreground, check the authorization state with that previous state.
Full code
Don't miss the last bit where previousAuthStatus is set
func checkStatus(_ status: CLAuthorizationStatus) {
switch status {
case .notDetermined:
// request "When in use"
locationManager.requestWhenInUseAuthorization()
case .authorizedWhenInUse:
// already tried requesting "Always"?
guard previousAuthStatus != .authorizedWhenInUse else {
<#notify the user...#>
break
}
// otherwise, request "Always"
locationManager.requestAlwaysAuthorization()
case .authorizedAlways:
// start updating location
<#dismiss any warning you may have displayed#>
locationManager.startUpdatingLocation()
case .denied:
<#notify the user...#>
case .restricted:
<#notify the user...#>
#unknown default:
break
}
// save status for next check
previousAuthStatus = status
}
I'm facing the same problem. This is not a problem for app updates (where you did not store authStatus in UserDefaults in previous version) only. It is also a problem if a user decides to uninstall and reinstall the app (user defaults are removed), or user manually change the status from 'Always' to 'When in Use' (no way of knowing that it was changed to 'When in Use' or if it was that status all along).
Btw: the user manually changing the authorisation is actually very common, because iOS now brings up an alert every once in a while "AppName has been using your location in the background... do you want to continue allowing this" (or something like that don't remember exactly). Lots of my users choose 'No' in that alert, resulting in the 'Always' location access being changed to 'When in Use' without the app getting a notification about that.
iOS however does remember if requestAlwaysAccess was already asked before, no matter what. Even after an uninstall and reinstall of the app.
So, with lack of other options I'm now using this, which honestly, is not the most user friendly, but it does work and is user friendly 'enough' (for me at least).
In stead of requesting always authorisation, I simply bring up an alert, with one of the buttons pointing to the apps Settings page, where user can then manually change the setting. I did add a userdefault storing if the app showed this alert before. If it did, I won't show it again.
switch CLLocationManager.authorizationStatus() {
case .restricted, .denied:
// show an alert with one button opening settings (see below)
case .authorizedAlways:
// already have always permission, continue with your code
case .notDetermined:
// request whenInUse authorisation (you can request always authorisation here too, but iOS won't show 'always' as a choice the first time)
case .authorizedWhenInUse:
guard !UserDefaults.showedNoAlwaysAuthorisationAlert else {
return
}
UserDefaults.showedNoAlwaysAuthorisationAlert = true
// show the alert with "no thanks" and "settings" button
// button action:
if 'settingsButtonTapped', let settingsUrl = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(settingsUrl) {
UIApplication.shared.open(settingsUrl, completionHandler: nil)
}
}

FaceID/TouchID success case keeps prompting for further authentication

I've implemented password/TouchID/FaceID on a view controller and when I hit the success case, I'd expect the prompt to stop firing but it just fires over and over again.
In my VC:
var context: LAContext!
func authenticateReturningUser() {
context = LAContext()
var error: NSError?
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
let reason = "Verify that this is your device to continue."
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, error in
DispatchQueue.main.sync {
guard success else {
guard let error = error else {
// show error
return
}
switch error {
case LAError.userCancel:
// do stuff
return
default: return
}
}
print("success")
}
}
}
}
The prompt should fire once and not again if the user successfully authorizes
Edit:
authenticateReturningUser is called from the AppDelegate's applicationDidBecomeActive function:
self.coverVC?.completionHandler = { self.removeBackgroundVC() }
self.coverVC?.authenticateReturningUser()
As far as I remember, when showing the Touch ID prompt, your app becomes inactive. So when the prompt is dismissed, your app becomes active again, triggering the App Delegate's applicationDidBecomeActive again.
You might consider introducing a flag that stores whether the app became inactive because of Touch ID / Face ID etc. or because of another reason and use it in applicationDidBecomeActive to decide if authentication should be triggered or not.
Where are you calling authenticateReturningUser()? You may want to create a static boolean authenticated that if false, allows the call to authenticateReturningUser(), and if true, skips the call, and set authenticated = true after calling the function once.

TouchID canceling should dismiss the view controller

For my app I need to save one page of it with TouchID. So the user is kinda forced to use TouchID or if the device does not support it, a password. If the user cancels the TouchID authentication, I want to View to disappear and get back to the root view. I already had that working, but somehow it does not work anymore and I really don't know why?! I just copied until the canceled option, the rest is does not matter I guess.
func authenticateUser() {
let context = LAContext()
var error: NSError?
let reasonString = "Authentication is needed to access your App"
if context.canEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, error: &error){
context.evaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, localizedReason: reasonString, reply: { (success, policyError) -> Void in
if success {
print("authentification successful")
}
}else{
switch policyError!.code{
case LAError.SystemCancel.rawValue:
print("Authentification was canceled by the system")
case LAError.UserCancel.rawValue:
print("Authentication was canceled by user")
self.navigationController?.dismissViewControllerAnimated(true, completion: nil)
//Yes I also tried popToRootViewController, still doesn't work
}
The documentation for the evaluatePolicy call says:
"Reply block that is executed when policy evaluation finishes. This block is evaluated on a private queue internal to the framework in an unspecified threading context."
So the problem is that you are trying to call navigation from the wrong thread. You need to make that call on the UI thread. For example:
dispatch_async(dispatch_get_main_queue()) {
// Navigation goes here
}

CNContactPicker grant permission not prompting

I'm updating my app to use CNContacts instead of AB. I've noticed that I am not getting prompted for granting permission to my Contacts. In the below switch, it is correctly telling me I am denied access - but then it doesn't prompt me to give it access. Furthermore, it just displays the picker and even stores the chosen properties into the tableview I am populating...
Is it not required to get permission to grab phone numbers or emails out of Contacts? I am confused why my code seems to be working if I am ".Denied"
//This code is called when you hit the "add a contact" button on my UI
switch CNContactStore.authorizationStatusForEntityType(.Contacts){
case .Authorized:
print("Already authorized")
presentPicker()
/* Access the address book */
case .Denied:
print("Denied access to address book")
store.requestAccessForEntityType(.Contacts){succeeded, err in
guard err == nil && succeeded else{
return
}
self.presentPicker()
}
case .NotDetermined:
store.requestAccessForEntityType(.Contacts){succeeded, err in
guard err == nil && succeeded else{
return
}
self.presentPicker()
}
default:
print("Not handled")
}
You do not need authorization to use CNContactsPickerViewController. It is "out of process"; it just works. In effect, it is the Contacts app sitting inside your app — and the user doesn't need permission to use the Contacts app.

Resources