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.
Related
I'd like to have the history section of an app locked for everyone besides the person who owns the phone. I don't like forcing the user to make an account or make a new PIN just for this app. Can I authorize using the PIN, Face ID or Touch ID he has already set up?
The Local Authentication framework will handle this.
Here is part of an Apple code sample:
/// Logs out or attempts to log in when the user taps the button.
#IBAction func tapButton(_ sender: UIButton) {
if state == .loggedin {
// Log out immediately.
state = .loggedout
} else {
// Get a fresh context for each login. If you use the same context on multiple attempts
// (by commenting out the next line), then a previously successful authentication
// causes the next policy evaluation to succeed without testing biometry again.
// That's usually not what you want.
context = LAContext()
context.localizedCancelTitle = "Enter Username/Password"
// First check if we have the needed hardware support.
var error: NSError?
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
let reason = "Log in to your account"
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason ) { success, error in
if success {
// Move to the main thread because a state update triggers UI changes.
DispatchQueue.main.async { [unowned self] in
self.state = .loggedin
}
} else {
print(error?.localizedDescription ?? "Failed to authenticate")
// Fall back to a asking for username and password.
// ...
}
}
} else {
print(error?.localizedDescription ?? "Can't evaluate policy")
// Fall back to a asking for username and password.
// ...
}
}
}
In my application, I attempt to login with Face ID using evaluatePolicy, and after 1 or 2 logins, I get an error in the callback. In that error, I call an async completion handler which calls another and eventually it calls canEvaluatePolicy. My app then freezes, allowing no further interaction with the UI. Why would this be happening? Does it have something to do with thread-safety? Some sample code that recreates this issue, can be found here
Here is a code snippet from that sample code for reference:
let localAuthenticationContext = LAContext()
if localAuthenticationContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) {
localAuthenticationContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "To access secure data") { success, evaluateError in
if success {
print("Success")
} else {
print("Face ID Error")
let context = LAContext()
let status = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
print("Status: \(status)")
}
}
}
I've been using Xcode 9.1 Beta 2 with the iPhone X simulator.
Actually, I building an app which contains local authentication.
My code so far:
func authenticateUser() {
let authenticationContext = LAContext()
var error: NSError?
let reasonString = "Touch the Touch ID sensor to unlock."
// Check if the device can evaluate the policy.
if authenticationContext.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error) {
authenticationContext.evaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, localizedReason: reasonString, reply: { (success, evalPolicyError) in
if success {
print("success")
} else {
if let evaluateError = error as NSError? {
// enter password using system UI
}
}
})
} else {
print("toch id not available")
// enter password using system UI
}
}
My problem is I want to use the passcode lock scene when the app doesn't has touch ID or invalid finger print.
Like below Image:
How can I do it?
You should use .deviceOwnerAuthentication instead of .deviceOwnerAuthenticationWithBiometrics to evaluate policy. With this parameter the system uses biometric authentication if available else it presents passcode screen. And if the biometric authentication is available but fails, a fallback button redirect to the passcode screen. See documentation :
If Touch ID or Face ID is available, enrolled, and not disabled, the user is asked for that first. Otherwise, they are asked to enter the device passcode.
Tapping the fallback button switches the authentication method to ask the user for the device passcode.
So your code will be:
func authenticateUser() {
let authenticationContext = LAContext()
var error: NSError?
let reasonString = "Touch the Touch ID sensor to unlock."
// Check if the device can evaluate the policy.
if authenticationContext.canEvaluatePolicy(LAPolicy.deviceOwnerAuthentication, error: &error) {
authenticationContext.evaluatePolicy( .deviceOwnerAuthentication, localizedReason: reasonString, reply: { (success, evalPolicyError) in
if success {
print("success")
} else {
// Handle evaluation failure or cancel
}
})
} else {
print("passcode not set")
}
}
At this point, I am afraid that you cannot access this passcode lock screen into your app, it is related to the iOS itself. You might need to build your own custom view controller to look/behave as the passcode lock scene (with Touch ID). I would suggest to use a library for achieving this, personally, I've tried PasscodeLock and it works fine for me.
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
}
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.