touchIDLockout deprecated in iOS 11.0

帅比萌擦擦* 提交于 2019-11-30 08:12:22

Short answer: It looks like a compiler bug to me, caused by the import of a C enumeration which defines multiple constants with the same value.

Long answer: Unfortunately I do not have a solution how to avoid the deprecation warning, only a possible explanation what causes it.

The LAError codes are defined as a C enumeration in <LAError.h> in the LocalAuthentication framework. Here is an extract of that definition:

// Error codes
#define kLAErrorAuthenticationFailed                       -1
#define kLAErrorUserCancel                                 -2
// ...
#define kLAErrorTouchIDNotAvailable                        -6
#define kLAErrorTouchIDNotEnrolled                         -7
#define kLAErrorTouchIDLockout                             -8
// ...
#define kLAErrorBiometryNotAvailable                        kLAErrorTouchIDNotAvailable
#define kLAErrorBiometryNotEnrolled                         kLAErrorTouchIDNotEnrolled
#define kLAErrorBiometryLockout                             kLAErrorTouchIDLockout

typedef NS_ENUM(NSInteger, LAError)
{
    LAErrorAuthenticationFailed = kLAErrorAuthenticationFailed,
    LAErrorUserCancel = kLAErrorUserCancel,
    // ...
    LAErrorTouchIDNotAvailable NS_ENUM_DEPRECATED(10_10, 10_13, 8_0, 11_0, "use LAErrorBiometryNotAvailable") = kLAErrorTouchIDNotAvailable,
    LAErrorTouchIDNotEnrolled NS_ENUM_DEPRECATED(10_10, 10_13, 8_0, 11_0, "use LAErrorBiometryNotEnrolled") = kLAErrorTouchIDNotEnrolled,
    LAErrorTouchIDLockout NS_ENUM_DEPRECATED(10_11, 10_13, 9_0, 11_0, "use LAErrorBiometryLockout")
    __WATCHOS_DEPRECATED(3.0, 4.0, "use LAErrorBiometryLockout") __TVOS_DEPRECATED(10.0, 11.0, "use LAErrorBiometryLockout") = kLAErrorTouchIDLockout,
    // ...
    LAErrorBiometryNotAvailable NS_ENUM_AVAILABLE(10_13, 11_0) __WATCHOS_AVAILABLE(4.0) __TVOS_AVAILABLE(11.0) = kLAErrorBiometryNotAvailable,
    LAErrorBiometryNotEnrolled NS_ENUM_AVAILABLE(10_13, 11_0) __WATCHOS_AVAILABLE(4.0) __TVOS_AVAILABLE(11.0) = kLAErrorBiometryNotEnrolled,
    LAErrorBiometryLockout NS_ENUM_AVAILABLE(10_13, 11_0) __WATCHOS_AVAILABLE(4.0) __TVOS_AVAILABLE(11.0) = kLAErrorBiometryLockout,
    // ...
} NS_ENUM_AVAILABLE(10_10, 8_0) __WATCHOS_AVAILABLE(3.0) __TVOS_AVAILABLE(10.0);

One can see that "old" (deprecated) and the "new" error codes use the same values. For example, both LAErrorTouchIDNotAvailable and LAErrorBiometryNotAvailable are defined as -6.

That is perfectly valid in C, but the raw values of a Swift enum must be mutually distinct. Apparently the Swift importer solves that by mapping the new/duplicate cases to static variables.

Here is an extract of the Swift mapping:

public struct LAError {

    public init(_nsError: NSError)
    public static var _nsErrorDomain: String { get }


    public enum Code : Int {
        case authenticationFailed
        case userCancel
        // ...
        @available(iOS, introduced: 8.0, deprecated: 11.0, message: "use LAErrorBiometryNotAvailable")
        case touchIDNotAvailable
        @available(iOS, introduced: 8.0, deprecated: 11.0, message: "use LAErrorBiometryNotEnrolled")
        case touchIDNotEnrolled
        @available(iOS, introduced: 9.0, deprecated: 11.0, message: "use LAErrorBiometryLockout")
        case touchIDLockout
        // ...
        @available(iOS 11.0, *)
        public static var biometryNotAvailable: LAError.Code { get }
        @available(iOS 11.0, *)
        public static var biometryNotEnrolled: LAError.Code { get }
        @available(iOS 11.0, *)
        public static var biometryLockout: LAError.Code { get }
        // ...
    }

    // ...
}

And this seems to be the cause for the deprecation warnings, and also for the problem reported on the swift-users mailing list

that it is impossible to write an exhaustive and warning-free switch statement for LAError.


To prove my conjecture, I have reproduced the problem with a custom enumeration: Add the following definition to the bridging header file of an macOS 10.13 or iOS 11 project:

#import <Foundation/Foundation.h>

typedef NS_ENUM(NSInteger, MyEnum)
{
    MyEnumA = 1,
    MyEnumB = 2,
    MyEnumC NS_ENUM_DEPRECATED(10_10, 10_13, 8_0, 11_0, "use MyEnumNewC") = 3,

    MyEnumNewC NS_ENUM_AVAILABLE(10_13, 11_0) = 3,
};

This is imported to Swift as

 public enum MyEnum : Int {
    case A
    case B
    @available(OSX, introduced: 10_10, deprecated: 10_13, message: "use MyEnumNewC")
    case C

    @available(OSX 10_13, *)
    public static var newC: MyEnum { get }
 }

with 3 cases for the first (distinct) enum values, and a static property for the duplicate value.

And indeed, any use of MyEnum triggers a deprecation warning:

// main.swift:
print(MyEnum.A) // Or: let _: MyEnum? = nil

// Build log:
// <unknown>:0: warning: 'C' was deprecated in OS X 10.13: use MyEnumNewC

In addition, it is not possible to use the new enum value in a switch statement:

func foo(err: MyEnum) {
    switch err {
    case .A:
        print("A")
    case .B:
        print("B")
    case .newC:
        print("C")
    }
}

// Build log:
// main.swift:12:9: error: switch must be exhaustive
// <unknown>:0: warning: 'C' was deprecated in OS X 10.13: use MyEnumNewC

even if the compiler (apparently) knows that these cases are exhaustive:

func foo(err: MyEnum) {
    switch err { // Switch must be exhaustive
    case .A:
        print("A")
    case .B:
        print("B")
    case .newC:
        print("C")
    default:
        print("default")
    }
}

// Build log:
// <unknown>:0: warning: 'C' was deprecated in OS X 10.13: use MyEnumNewC
// main.swift:19:9: warning: default will never be executed

This looks like a compiler bug to me.

Yes, these are new warnings which are present as Apple moves to iOS 11 and FaceID. Most likely, you are checking to see if the biometric hardware is not locked out, has enrolled fingerprints, and the device has the supporting hardware.

Here an example set up:

import LocalAuthentication

...

var authContext = LAContext()
var biometricsError: NSError?
authContext?.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &biometricsError)

Up to iOS 10, one would run checks like this:

if biometricsError?.code == LAError.touchIDNotAvailable.rawValue {
  // No hardware
}

if biometricsError?.code == LAError.touchIDNotEnrolled.rawValue {
  // No fingerprints
}

if biometricsError?.code == LAError.touchIDLockout.rawValue {
  // Locked out
}

Note: iOS 11 introduced a slight variant of the above code. Instead of using LAError.touchID for each error property, they introduced LAError.biometry. Therefore you would have: biometryNotAvailable, biometryNotEnrolled, and biometryLockout.

Apple seems to prefer this approach, instead:

if biometricsError?.code == Int(kLAErrorBiometryNotAvailable) {
  // No hardware
}

if biometricsError?.code == Int(kLAErrorBiometryNotEnrolled) {
  // No fingerprints
}

if biometricsError?.code == Int(kLAErrorBiometryLockout) {
  // Locked out
}

This method gets rid of Xcode's warnings.

As has been noted, this is a bug in the compiler. The Swift team are aware and you might want to go and vote for the bug. At the same time, add a watch on it so that you can remove the following workaround when it has been fixed.

What you need to do in order to not get the warnings is: do not mention LAError. Think of LAError as Voldemort.

Instead, use Objective-C style error checking. All Error enums map to an NSError, which are built up from a domain and a code. The constants to compare these with are also exported to Swift. They can be named without warnings. So your code might look a little (hopefully, very little) like this.

let context = LAContext()
let text = "Authenticate, please!"
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: text) { (success, error) in
    if success {
        print("🎉")
    } else {
        guard let error = error else {
            return print("Should not happen according to the docs!")
        }
        let nsError = error as NSError
        switch nsError.domain {
        case kLAErrorDomain:
            switch nsError.code {
            case Int(kLAErrorUserCancel):
                print("User cancelled.")
            case Int(kLAErrorBiometryLockout):
                print("Biometry lockout.")
            default:
                print("Unhandled error.")
            }
        default:
            print("Unhandled error domain. Probably will not happen.")
        }
    }
}

I too struggled with this for a very long time. Oliver's answer led me to a solution that worked for me, but just in case there are others that are still having this issue - it seems that ANY reference to the old LAError values will produce the warnings listed above.

For example, this simple code produces the warnings. Remove ALL reference to the old codes and use Oliver's approach.

func evaluateAuthenticationPolicyMessageForLA(errorCode: Int) -> String {
    var message = ""

    switch errorCode {
    case LAError.authenticationFailed.rawValue:
        message = "The user failed to provide valid credentials"
    default:
        message = "Can't use any version of old LAError"
    }

    return message
}//evaluateAuthenticationPolicyMessageForLA

In fact, it can be even simpler. Try this:

func evaluateAuthenticationPolicyMessageForBiometricsError(biometricsError: Int) -> String {

    var message = "I would normally leave this blank"

    if biometricsError == kLAErrorBiometryNotAvailable {
        message = "Biometrics are not available on this device"
    }//if not avail

    if biometricsError == kLAErrorBiometryNotEnrolled {
        message = "Biometrics are not enrolled on this device"
    }//if not enrolled

    if biometricsError == kLAErrorBiometryLockout {
        message = "Biometrics are locked out on this device"
    }//if locked out

    return message

}//evaluateAuthenticationPolicyMessageForBiometricsError
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!