How to detect when user used Password AutoFill on a UITextField
Asked Answered
E

8

23

I've implemented all the app and server changes necessary to support Password Autofill on iOS 11, and it works well. I'd like it to work a little better.

My username and password fields are UITextFields. I would like to identify when a user has "autofilled" one of the two UITextFields, so I can progress to the next step. Currently the user autofills an item, then needs to press the Next button on the on-screen keyboard in order to advance. I'd like to trigger this on behalf of the user.

The WWDC2017 Password Autofill session says to use UITextFieldTextDidChange. This works, but of course this is also triggered when a user is manually typing in those fields.

My thought has been to compare the prior version of the text with the new version of the text, and assume that if the length has increased from zero to greater than some minimal length (2 or more), the user used autofill. That should work most of the time, but has a risk of a false trigger (fast typing on slow device perhaps). So to me, this may be a risky assumption.

I'm curious is anyone has found a more surefire way to determine if Password Autofill has been used on a UITextField, or just thinks my worry about a false trigger is unfounded.

Eindhoven answered 24/9, 2017 at 15:38 Comment(2)
I'd also like to know if users actually use Password AutoFill, by adding usage to the app's analytics. We do the same with 1Password.Eindhoven
is this still not solvable?Mischiefmaker
M
14

Not sure if the previous answer stopped working at some point, but I can't get it to work—I only get a single didBeginEditing call when AutoFill is used.

However, I did find a way to detect AutoFill. And keep in mind that it is possible for AutoFill to be used after some characters have already been entered, for example if the user has already typed some numbers in the phone number, then they AutoFill the full number.

For Swift 4/5, add the following to the delegate of the UITextField:

private var fieldPossibleAutofillReplacementAt: Date?

private var fieldPossibleAutofillReplacementRange: NSRange?

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    // To detect AutoFill, look for two quick replacements. The first replaces a range with a single space
    // (or blank string starting with iOS 13.4).
    // The next replaces the same range with the autofilled content.
    if string == " " || string == "" {
        self.fieldPossibleAutofillReplacementRange = range
        self.fieldPossibleAutofillReplacementAt = Date()
    } else {
        if fieldPossibleAutofillReplacementRange == range, let replacedAt = self.fieldPossibleAutofillReplacementAt, Date().timeIntervalSince(replacedAt) < 0.1 {
            DispatchQueue.main.async {
                // Whatever you use to move forward.
                self.moveForward()
            }
        }
        self.fieldPossibleAutofillReplacementRange = nil
        self.fieldPossibleAutofillReplacementAt = nil
    }

    return true
}
Mickelson answered 25/11, 2018 at 6:45 Comment(3)
Function is not called for me at all.Barbur
it seems that on iOS 13.4 it uses empty string "" instead of space " "Lurlene
Thanks, I've updated my answer to cover the empty string behavior.Mickelson
H
6

Found a solution.

When the password manager is used to autofill username + password, it will trigger didBeginEditing twice, faster than a human ever could.

So, I calculate the time between the events. If the time is extremely fast, then I assume that autofill (e.g. FaceID or TouchID) was used to enter credentials and auto-trigger whatever UI is next -- in my case, the User tapping "Sign-in".

Obviously, you have to set up the correct delegation of the UITextFields you want to monitor, but once you do that:

var biometricAutofillTime: Date!

func textFieldDidBeginEditing(_ textField: UITextField) {
    if biometricAutofillTime != nil {
        if Date().timeIntervalSince(biometricAutofillTime) < 0.1 {
            // NOTE: Need to hesitate for a very short amount of time,
            //        because, otherwise, the second UITextField (password)
            //        won't yet be populated
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { self.didTapSignin() }
        }
        biometricAutofillTime = nil
    }
    biometricAutofillTime = Date()
}
Humidify answered 18/11, 2018 at 23:21 Comment(0)
F
3

This detects when user has autofilled via passwords. It may also trigger when user pastes text from their clipboard. (If textfield is empty)

You can probably handle the logic to remove user pasted cases with this link.. how to know when text is pasted into UITextView

  private var didAutofillTextfield: Bool = false {
    didSet {
      if didAutofillTextfield {
        // Fire analytics for user autofilling
      }
    }
  }


 func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    // If the range is {0,0} and the string count > 1, then user copy paste text or used password autofill.
    didAutofillTextfield = range == NSRange(location: 0, length: 0) && string.count > 1
    return true
  }
Fricative answered 26/11, 2019 at 22:36 Comment(0)
P
1

I used this delegate method:

func textFieldDidChangeSelection(_ textField: UITextField) {
    // call when user select something
}

from documentation:

Method textFieldDidChangeSelection(_:) Tells the delegate when the text selection changes in the specified text field.te

Previdi answered 16/7, 2021 at 12:50 Comment(0)
T
0

I don't think there is a better solution.

One thing I noticed is that autofill is only enabled when the text field is empty.

So if the text field went from empty to a length greater than the minimum password/username, then it is most likely autofill/paste.

I am using shouldChangeCharactersIn to detect the change in the UITextField. I'm not for sure if there is a case where text from the keyboard could be batched together before the delegate method is called.

Taverner answered 5/10, 2017 at 13:38 Comment(1)
This is not true anymore. Autocomplete does need a blank field, it replaces any existing text.Malleus
P
0

I'd like to trigger this on behalf of the user.

If this is your primary goal, I'm doing a little different approach here.

Upon showing the login form, I first check iCloud Keychain with SecRequestSharedWebCredential. If the closure returns a credentials, which means user's intent is to login with it, then I automatically login for him/her. Otherwise, make the login text filed becomeFirstResponder().

This approach does not support third-party password manager, but most people use iCloud Keychain I believe.

Prudhoe answered 20/6, 2019 at 3:58 Comment(0)
G
-1

Simply add the following extension. All you need to do is to use any library to get hex from backgroundColor. That is all.

import UIKit

extension UITextField {
    var isAutoFilled: Bool {
        let filtered = subviews.filter { $0.backgroundColor?.hex == "FAFFBD" || !$0.isUserInteractionEnabled }
        return !filtered.isEmpty && text!.count >= 3
    }
}
Garrulous answered 4/3 at 20:57 Comment(0)
S
-4

I found another simple way.. Hope it will help who is looking for it.

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// Usually string is coming as single character when user hit the keyboard .. 
// but at AutoFill case it will come as whole string which recieved .. 
// at this moment you can replace what you currently have by what you have recieved.
// In below case I'm expecting to recieve 4 digits as OTP code .. you can change it by what you are expecting to recieve.
    if string.count == 4 {
            doWhatEverYouByAutoFillString(text: string)
            return true
        } 
    }
}
Sheedy answered 22/7, 2019 at 13:4 Comment(2)
I'm receiving an empty string (just the whitespace) when tap on suggestion to complete. Security things?Joyless
You will receive the right message after this whitespace so don't do anything at this moment .. just try to use the above method and please tell us if it's working with you or not.Sheedy

© 2022 - 2024 — McMap. All rights reserved.