skip to Main Content

I have a working example below, but a bit of an explanation.

I want the user to be able to toggle the option to unlock their app data with biometrics (or not if preferred). If they activate the toggle, once the app resigns to background or has been terminated the next time it is launched they should be prompted to log in.

This portion of the app functionality I have operational. However, once the user logs in once, resigns to background and then relaunches they are in instantly.

I altered the codebase so that the "permission" bool was set to false, however when the view to authenticate prompts them, there is none of the Apple biometrics, they are simply granted access.

I tried using the LAContext.invalidate but after adding that into the check when resigning of background the biometric prompts never reappear – unless fully terminated.

Am I missing something or how do other apps like banking create the prompt on every foreground instance?

// main.swift
@main
struct MyApp: App {
  @StateObject var biometricsVM = BiometricsViewModel()
  var body: some Scene {
    WindowGroup {
      // toggle for use
      if UserDefaults.shared.bool(forKey: .settingsBiometrics) {
        // app unlocked
        if biometricsVM.authorisationGranted {
          MyView() // <-- the app view itself
           .onAppear {
             NotificationCenter.default.addObserver(
               forName: UIApplication.willResignActiveNotification,
               object: nil,
               queue: .main
             ) { _ in
               biometricsVM.context.invalidate()
               biometricsVM.authorisationGranted = false
             }
           }
        } else {
          BioCheck(vm: biometricsVM)
        }
      }
    }
  }
}
// biometricsVM.swift
final class BiometricsViewModel: ObservableObject {
  @Published var authorisationGranted = false
  @Published var authorisationError: Error?

  let context = LAContext()

  func requestAuthorisation() {
    var error: NSError? = nil
    let hasBiometricsEnabled = context.canEvaluatePolicy(
      .deviceOwnerAuthentication, error: &error
    )

    let reason = "Unlock to gain access to your data"

    if hasBiometricsEnabled {
      switch context.biometryType {
        case .touchID, .faceID:
          context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason ) { success, error in
            DispatchQueue.main.async {
              self.authorisationGranted = success
              self.authorisationError = error
            }
          }

         case .none:
          // other stuff 

        @unknown default:
          // other stuff 
      }
    }
  }
}
// biocheck.swift
struct BioCheck: View {
  @ObservedObject var vm: BiometricsViewModel
  var body: some View {
    Button {
     vm.requestAuthorisation()
    } label: {
     Text("Authenticate")
    }
    .onAppear { vm.requestAuthorisation() }
  }
}

Video of issue:

2

Answers


  1. The problem is that the code in MyApp runs once the app opens similar to didFinishLaunchingWithOptions. To fix this, create a new View & place the following code in it:

    if UserDefaults.shared.bool(forKey: .settingsBiometrics) {
        if biometricsVM.authorisationGranted {
            MyView()
                .onAppear {
                    NotificationCenter.default.addObserver(
                        forName: UIApplication.willResignActiveNotification,
                        object: nil,
                        queue: .main
                    ) { _ in
                        biometricsVM.context.invalidate()
                        biometricsVM.authorisationGranted = false
                    }
                }
        } else {
            BioCheck(vm: biometricsVM)
        }
    }
    

    Then replace the content of WindowGroup with the View you created.

    Edit:

    It was the function requestAuthorisation giving an error related to context. You should create a new context every time you call that function:

      func requestAuthorisation() {
        var error: NSError? = nil
        let context = LAContext()
        let hasBiometricsEnabled = context.canEvaluatePolicy(
            .deviceOwnerAuthentication, error: &error
        )
        
        let reason = "Unlock to gain access to your data"
        
        if hasBiometricsEnabled {
            switch context.biometryType {
            case .touchID, .faceID:
                context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason ) { success, error in
                    DispatchQueue.main.async {
                        self.authorisationGranted = success
                        self.authorisationError = error
                    }
                }
                
            case .none:
                // other stuff
                break
            @unknown default:
                break
            }
        }
    }
    
    Login or Signup to reply.
  2. create a new LAContext variable each time you want to authenticate, don’t use the same global variable. Since the LAContext needs one successful authentication alone.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search