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.
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
}
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()
}
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
}
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
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.
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.
Swift 5:
The following delegate method gets triggered every time that the user types something in the textfield. That string count is usually one, so any number great than that is either autofill or the user pasting some text into the field. I am only using this delegate on this password field.
extension LoginView: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if string.count > 1 {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self] in
self?.endEditing(true)
}
}
return true
}
}
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
}
}
}
Related
I want to disable manual text entering from one-time-code textField while the user only can tap SMS OTP Code from Keyboard Quicktype Bar.
Another question i got from seeing whatsapp is that their input shows the Keyboard Quicktype bar automatically while mine is not unless i call becomeFirstResponder
How can i achieve this?
Thanks.
You can try this, maybe it works for your use case
Remove textField.isEnabled = false if you added it before
Add textField.delegate = self so we can manage what happens when user adds input
Add textField.becomeFirstResponder() to make the keyboard appear
Then implement this UITextFieldDelegate callback
extension YourViewOrViewController: UITextFieldDelegate {
func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
// Only allow multiple characters to be set like the OTP
// Or define your own logic when you want text to be
// accepted into the text field
return string.count != 1
}
}
Check if this gives you the desired result
In my project when user select specific UITextField (that UITextField supposed to get user telephone number), the QuickType Keyboard show user telephone number. I want when user select his/her telephone number I can change that (remove "+" in telephone number) and show the result in that UITextField. how can I do that?
UPDATE:
I tried shouldChangeCharactersIn (UITextFieldDelegate function) to handle that, but replacementString return space (" ") and if I just return true (doing nothing inside that function) to that nothing will show inside UITextField.
You were on the right track with shouldChangeCharactersIn, but in case with QuickType keyboard it gets called two times instead of one.
First call is made to clean the current string, even when it's empty: range is (0, 0) in this case. Not much sence, but if you type something, select whole text, and paste phone number from quick help, this first change will have range of the full string to clear it.
And to modify input string, you need to update text field text and return false, because you don't need system to update text anymore.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if string.starts(with: "+") {
let modifierString = String(string.dropFirst())
.trimmingCharacters(in: .whitespacesAndNewlines)
textField.text = ((textField.text ?? "") as NSString)
.replacingCharacters(in: range, with: modifierString)
return false
} else {
return true
}
}
Note that it'll also ignore press of "+" made by user, if you don't want that you probably need to add more logic to that if, like
if string.count > 1 && string.starts(with: "+") {
This will allow user to press "+" but remove it from any pasted content.
If your objective is to remove any extra characters and keep only numbers, you could in the first place restrict the user to inputting only numbers by selecting the "Number Pad" keyboard type in the attributes inspector of the text field in the storyboard.
Or if you'd like to do it with code:
textField.keyboardType = .numberPad
Sorry if this is not what you are looking for, I just thought maybe you overcomplicated the problem and resorted to filtering the text to numbers, when there is an Apple-provided type of keyboard exactly for collecting phone numbers that you could use instead.
The autofill works incredibly well, but I have one scenario when it isn't optimal and I am not aware of a work around a perhaps a proper way of doing it.
I have a screen with 2 UITextFields, first one is Amount, a monetary value to be transferred to another person. The second field is the password, the user will need to re-enter his password before the transaction can be completed.
The first field (Amount) has content type set as .unspecified and the second field (password) has it set to .password
When the user taps the second field Autofill beautifully suggest the the password to be used, but once the correct password is tapped, iOS automatically assumes the first field (amount) is the username and fills it with the username associated with the selected account, overwriting the amount the user had previously entered.
Is there a way to force password only autofill?
Today i face the same issue, with some different way. My situation is taking input of mobile number and password entry into the login page. Native app only support for mobile number while the web app support for email only. So while iOS autofill is in action, it fill the mobile number field with the email field, which is not acceptable.
After playing sometime with the autofill, i have found the life-cycle of the UITextField delegate is somewhat different in case of autofill.
When a autofill is tapped which is provided in the top of the keyboard, the UITextFieldDelegate start working from the beginning. Although the keyboard is open the delegate method started from the current with call order as follows
textField(_:shouldChangeCharactersIn:replacementString:)
textFieldShouldEndEditing(_:)
This delegate calls without the keyboard dismissing and re-appear again. This is unusual. Returning false in the textField(_:shouldChangeCharactersIn:replacementString:) has no effect in this case.
So theoretically i have the chance to edit the mobile field in the textFieldShouldEndEditing. To do that i keep track of the text which was present before the autofill begin. so took two variable as follows
var previousText: String?
var nextText: String?
Whenever a UITextField begin editing, i save it in previousText as following
public func textFieldDidBeginEditing(_ textField: UITextField) {
previousText = textField.text
}
then i track the changes inside the textField(_:shouldChangeCharactersIn:replacementString:) as following
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
nextText = self.getCompleteString(original: textField.text, replacingRange: range, withString: string)
// YOU MAY RETURN `true` OR `false` BASED ON KEYBOARD TYPING, BUT RETURNING `false` IN CASE OF AUTOFILL HAS NO EFFECT. SO I ASSUME, YOU RETURN `true` ALWAYS.
return true;
}
func getCompleteString(original: String?, replacingRange: NSRange, withString: String) -> String? {
guard var originalText = original else {
return nil
}
guard let range = Range<String.Index>.init(replacingRange, in: originalText) else {
return nil
}
originalText.replaceSubrange(range, with: withString)
return originalText
}
Now the most interesting part, detecting custom requirement (in my case detecting a possible valid mobile number, where your case was detecting a valid amount)
func textFieldDidEndEditing(_ textField: UITextField) {
if textField == MY_MOBILE_TEXT_FIELD {
if nextText.IS_POSSIBLE_VALID_MOBILE_NUMBER() { // my function to detect the possible valid mobile number
textField.text = nextText
} else {
textField.text = previousText
}
}
}
It worked for me, hope it works for you too.
Autofill disregards what textfield delegate methods return
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
So can try another hack which worked for me,
You can set textContentType of amount text field to password type and use a toolbar on the keyboard to side autofill suggestions(Since you would be using num pad for entering digits in the amount field so you might already be using a custom toolbar).
amount_text_field.textContentType = .password
This way, iOS will assume it to be a password textfield and would not fill usernames or passwords which were applied from other textfields. And using toolbar should help you hide autofill suggestions on this textfield.
Evening, in my app I have several UITextfield. Each one has to confirm to different limitations.
For example, I have a date Field, zipCode Field, SSN Field etc.
From the Apple Documentation I found:
Assign a delegate object to handle important tasks, such as:
Determining whether the user should be allowed to edit the text field’s contents.
Validating the text entered by the user.
Responding to taps in the keyboard’s return button.
Forwarding the user-entered text to other parts of your app.
Store a reference to the text field in one of your controller objects.
So I'm pretty sure I have to use delegates and func textFieldDidEndEditing(_:).
The only way that came to my mind is to use a switch statement inside the func textFieldDidEndEditing(_:) to confirm the delegate to the difference limitation.
Is there a better, safer and faster pattern to face this problem?
You can set unique tag to your every text field and can compare in textFieldDidEndEditing or you can take IBOutlet of every textField and can compare it in textFieldDidEndEditing like,
func textFieldDidEndEditing(textField: UITextField) {
// By tag
if textField.tag == 100 {
}
// OR
//by outlet
if textField == self.myTextField {
}
}
You are right, you will have to check the textfield, either you can check tags assigned for different text fields using switch statement like you said,
or you can compare textfields itself,
if textfield1,textfield2 are outlets to two text fields, you can compare as following,
func textFieldDidEndEditing(textField: UITextField)
{
if textField == textfield1
{
}
else if textField == textfield2
{
}
}
you can create enum for validation
enum Type {
case zipcode
case number
}
then you can create a method for validation like this :
func isValidate(text: String, type: Type) -> Bool {
switch type {
case .zipcode:
...
}
}
this method can be in Util class. this is best practice. because your logic is encapsulate from out .
If you need more control over the text which is committed to the text field or if you want to provide feedback while the text field is being edited, you should implement a different delegate instead:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// Build and check the new text value. Accept or reject the change.
}
In the delegate, you can build the new text value. Validate it, apply constraints, decide on which feedback the user should receive ("Your password must be at least eight characters long", "This is not a valid IBAN"), accept or reject the change (i.e. return false).
Keep in mind, that the delegate is not called if you assign the text property manually. Moreover, this delegate is called when text is pasted into or deleted from the text field, which can make matters much more complicated depending on what you do.
I'm developing an app for Ipad. I'm designing a forgot password screen to allow user to enter password to UITextField. By design, the password only allow numeric input. I can set UITextFiled keyboardtype to be phonepad in Iphone but the option seem not working for Ipad (Ipad always show the full keyboard layout). How can we achieve the keyboard for Ipad app that only have number?
Do I have to design my own keyboard layout?
Any help is much appreciate. Thanks!
The keyboard type does not dictate what sort of input the textfield accepts, even if you use a custom keyboard that only displays numbers, the user can always paste something or use an external hardware keyboard.To do that, you need to observe the input, for example, by becoming the UITextFieldDelegate and then:
Example in swift:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool{
// non decimal digit character set, better save this as a property instead of creating it for each keyboard stroke
let non_digits = NSCharacterSet.decimalDigits.inverted
// Find location for non digits
let range = string.rangeOfCharacter(from: non_digits)
if range == nil { // no non digits found, allow change
return true
}
return false // range was valid, meaning non digits were found
}
This will prevent any non digit character from being added to the textfield.
There is not a built in number-only (phone/pin) keyboard on iPad
You need to implement your own keyboard if you want this on iPad.
There are many examples out there:
https://github.com/azu/NumericKeypad
https://github.com/lnafziger/Numberpad
https://github.com/benzado/HSNumericField
Yes I was facing the same for iPad hence I used this:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// to avoid any other characters except digits
return string.rangeOfCharacter(from: CharacterSet(charactersIn:"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!##$%^&*()-=_+`~[{]}|\\: ;\"/?>.<,'")) == nil
}