UITextField with secure entry, always getting cleared before editing

走远了吗. 提交于 2019-12-17 08:25:09

问题


I am having a weird issue in which my UITextField which holds a secure entry is always getting cleared when I try to edit it. I added 3 characters to the field, goes to another field and comes back, the cursor is in 4th character position, but when I try to add another character, the whole text in the field gets cleared by the new character. I have 'Clears when editing begins' unchecked in the nib. So what would be the issue? If I remove the secure entry property, everything is working fine, so, is this the property of Secure entry textfields? Is there any way to prevent this behaviour?


回答1:


Set,

textField.clearsOnBeginEditing = NO;

Note: This won't work if secureTextEntry = YES. It seems, by default, iOS clears the text of secure entry text fields before editing, no matter clearsOnBeginEditing is YES or NO.




回答2:


If you don't want the field to clear, even when secureTextEntry = YES, use:

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {

    NSString *updatedString = [textField.text stringByReplacingCharactersInRange:range withString:string];

    textField.text = updatedString;

    return NO;
}

I encountered a similar issue when adding show/hide password text functionality to a sign-up view.




回答3:


If you're using Swift 3, give this subclass a go.

class PasswordTextField: UITextField {

    override var isSecureTextEntry: Bool {
        didSet {
            if isFirstResponder {
                _ = becomeFirstResponder()
            }
        }
    }

    override func becomeFirstResponder() -> Bool {

        let success = super.becomeFirstResponder()
        if isSecureTextEntry, let text = self.text {
            self.text?.removeAll()
            insertText(text)
        }
        return success
    }

}

Why does this work?

TL;DR: if you're editing the field while toggling isSecureTextEntry, make sure you call becomeFirstResponder.


Toggling the value of isSecureTextEntry works fine until the user edits the textfield - the textfield clears before placing the new character(s). This tentative clearing seems to happen during the becomeFirstResponder call of UITextField. If this call is combined with the deleteBackward/insertText trick (as demonstrated in answers by @Aleksey and @dwsolberg), the input text is preserved, seemingly canceling the tentative clearing.

However, when the value of isSecureTextEntry changes while the textfield is the first responder (e.g., the user types their password, toggles a 'show password' button back and forth, then continues typing), the textfield will reset as usual.

To preserve the input text in this scenario, this subclass triggers becomeFirstResponder only if the textfield was the first responder. This step seems to be missing in other answers.

Thanks @Patrick Ridd for the correction!




回答4:


@Eric's answer works but I had two issues with it.

  1. As @malex pointed out, any change of text in middle will place carriage at the end of text.
  2. I'm using rac_textSignal from ReactiveCocoa and changing the text directly wouldn't trigger the signal.

My final code

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    //Setting the new text.
    NSString *updatedString = [textField.text stringByReplacingCharactersInRange:range withString:string];
    textField.text = updatedString;

    //Setting the cursor at the right place
    NSRange selectedRange = NSMakeRange(range.location + string.length, 0);
    UITextPosition* from = [textField positionFromPosition:textField.beginningOfDocument offset:selectedRange.location];
    UITextPosition* to = [textField positionFromPosition:from offset:selectedRange.length];
    textField.selectedTextRange = [textField textRangeFromPosition:from toPosition:to];

    //Sending an action
    [textField sendActionsForControlEvents:UIControlEventEditingChanged];

    return NO;
}

Swift3 addition by @Mars:

  func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    let nsString:NSString? = textField.text as NSString?
    let updatedString = nsString?.replacingCharacters(in:range, with:string);

    textField.text = updatedString;


    //Setting the cursor at the right place
    let selectedRange = NSMakeRange(range.location + string.length, 0)
    let from = textField.position(from: textField.beginningOfDocument, offset:selectedRange.location)
    let to = textField.position(from: from!, offset:selectedRange.length)
    textField.selectedTextRange = textField.textRange(from: from!, to: to!)

    //Sending an action
    textField.sendActions(for: UIControlEvents.editingChanged)

    return false;
}



回答5:


@Thomas Verbeek's answer helped me a lot:

class PasswordTextField: UITextField {

    override var isSecureTextEntry: Bool {
        didSet {
            if isFirstResponder {
                _ = becomeFirstResponder()
            }
        }
    }

    override func becomeFirstResponder() -> Bool {

        let success = super.becomeFirstResponder()
        if isSecureTextEntry, let text = self.text {
            deleteBackward()
            insertText(text)
        }
        return success
    }

}

Except I did find a bug in my code with it. After implementing his code, if you have text in the textField and you tap on the textField box, it will only delete the first char and then insert all of the text in again. Basically pasting the text in again.

To remedy this I replaced the deleteBackward() with a self.text?.removeAll() and it worked like a charm.

I wouldn't have gotten that far without Thomas' original solution, so thanks Thomas!




回答6:


I had a similar problem. I have login (secureEntry = NO) and password (secureEntry = YES) text fields embedded in a table view. I tried setting

textField.clearsOnBeginEditing = NO;

inside both of the relevant delegate methods (textFieldDidBeginEditing and textFieldShouldBeginEditing), which didn't work. After moving from the password field to the login field, the whole login field would get cleared if I tried to delete a single character. I used the textFieldShouldChangeCharactersInRange to solve my problem, as follows:

-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    if (range.length == 1 && textField.text.length > 0) 
    {
        //reset text manually
        NSString *firstPart = [textField.text substringToIndex:range.location]; //current text minus one character
        NSString *secondPart = [textField.text substringFromIndex:range.location + 1]; //everything after cursor
        textField.text = [NSString stringWithFormat:@"%@%@", firstPart, secondPart];

        //reset cursor position, in case character was not deleted from end of 
        UITextRange *endRange = [textField selectedTextRange];
        UITextPosition *correctPosition = [textField positionFromPosition:endRange.start offset:range.location - textField.text.length];
        textField.selectedTextRange = [textField textRangeFromPosition:correctPosition toPosition:correctPosition];

        return NO;
    }
    else
    {
        return YES;
    }
}

The range.length == 1 returns true when the user enters a backspace, which is (strangely) the only time that I would see the field cleared.




回答7:


I've tried all solutions here and there and finally came with that overriding:

- (BOOL)becomeFirstResponder
{
    BOOL became = [super becomeFirstResponder];
    if (became) {
        NSString *originalText = [self text];
        //Triggers UITextField to clear text as first input
        [self deleteBackward];

        //Requires setting text via 'insertText' to fire all associated events
        [self setText:@""];
        [self insertText:originalText];
    }
    return became;
}

It triggers UITextField's clear and then restores original text.




回答8:


Swift 5

yourtextfield.clearsOnInsertion = false
yourtextfield.clearsOnBeginEditing = false

Note: This won't work if secureTextEntry = YES. It seems, by default, iOS clears the text of secure entry text fields before editing, no matter clearsOnBeginEditing is YES or NO.

Easy way use class and its work 100%

class PasswordTextField: UITextField {

    override var isSecureTextEntry: Bool {
        didSet {
            if isFirstResponder {
                _ = becomeFirstResponder()
                //MARK:- Do something what you want
            }
        }
    }

    override func becomeFirstResponder() -> Bool {

        let success = super.becomeFirstResponder()
        if isSecureTextEntry, let text = self.text {
            self.text?.removeAll()
            insertText(text)
        }
         return success
    }
}



回答9:


Based on Aleksey's solution, I'm using this subclass.

class SecureNonDeleteTextField: UITextField {

    override func becomeFirstResponder() -> Bool {
        guard super.becomeFirstResponder() else { return false }
        guard self.secureTextEntry == true else { return true }
        guard let existingText = self.text else { return true }
        self.deleteBackward() // triggers a delete of all text, does NOT call delegates
        self.insertText(existingText) // does NOT call delegates
        return true
    }
}

Changing the replace characters in range doesn't work for me because I'm doing other things with that at times, and adding more makes it more likely to be buggy.

This is nice because it pretty much works perfectly. The only oddity is that the last character is shown again when you tap back into the field. I actually like that because it acts as a sort of placeholder.




回答10:


If you want to use secureTextEntry = YES and proper visual behavior for carriage, you need this:

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
{

    if (!string.length) {
        UITextPosition *start = [self positionFromPosition:self.beginningOfDocument offset:range.location];
        UITextPosition *end = [self positionFromPosition:start offset:range.length];
        UITextRange *textRange = [self textRangeFromPosition:start toPosition:end];
        [self replaceRange:textRange withText:string];
    }
    else {
       [self replaceRange:self.selectedTextRange withText:string];
    }

    return NO;
}



回答11:


After playing with solution from @malex I came to this Swift version:

1) Don't forget to make your view controller a UITextFieldDelegate:

class LoginViewController: UIViewController, UITextFieldDelegate {
...
}

override func viewDidLoad() {
   super.viewDidLoad()
   textfieldPassword.delegate = self
}

2) use this:

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    if let start: UITextPosition = textField.positionFromPosition(textField.beginningOfDocument, offset: range.location),
       let end: UITextPosition = textField.positionFromPosition(start, offset: range.length),
       let textRange: UITextRange = textField.textRangeFromPosition(start, toPosition: end) {
           textField.replaceRange(textRange, withText: string)
    }
    return false
}

...and if you are doing this for the "show/hide password" functionality most probably you will need to save and restore caret position when you switch secureTextEntry on/off. Here's how (do it inside the switching method):

var startPosition: UITextPosition?
var endPosition: UITextPosition?

// Remember the place where cursor was placed before switching secureTextEntry
if let selectedRange = textfieldPassword.selectedTextRange {
   startPosition = selectedRange.start
   endPosition = selectedRange.end
}

...

// After secureTextEntry has been changed
if let start = startPosition {
   // Restoring cursor position
   textfieldPassword.selectedTextRange = textfieldPassword.textRangeFromPosition(start, toPosition: start)
   if let end = endPosition {
       // Restoring selection (if there was any)
       textfieldPassword.selectedTextRange = textfield_password.textRangeFromPosition(start, toPosition: end)
       }
}



回答12:


We solved it based on dwsolberg's answer with two fixes:

  • the password text was duplicated when tapping into an already focused password field
  • the last character of the password was revealed when tapping into the password field
  • deleteBackward and insertText cause the value changed event to be fired

So we came up with this (Swift 2.3):

class PasswordTextField: UITextField {

    override func becomeFirstResponder() -> Bool {
        guard !isFirstResponder() else {
            return true
        }
        guard super.becomeFirstResponder() else {
            return false
        }
        guard secureTextEntry, let text = self.text where !text.isEmpty else {
            return true
        }

        self.text = ""
        self.text = text

        // make sure that last character is not revealed
        secureTextEntry = false
        secureTextEntry = true

        return true
    }
}



回答13:


This is swift code from my project is tested and deals with, backspace and secure-entry changes false/true

// get the user input and call the validation methods

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

    if (textField == passwordTextFields) {

        let newString = (textField.text! as NSString).replacingCharacters(in: range, with: string)

        // prevent backspace clearing the password
        if (range.location > 0 && range.length == 1 && string.characters.count == 0) {
            // iOS is trying to delete the entire string
            textField.text = newString
            choosPaswwordPresenter.validatePasword(text: newString as String)

            return false
        }

        // prevent typing clearing the pass
        if range.location == textField.text?.characters.count {
            textField.text = newString
            choosPaswwordPresenter.validatePasword(text: newString as String)

            return false
        }

        choosPaswwordPresenter.validatePasword(text: newString as String)
    }

    return true
}



回答14:


IOS 12, Swift 4

  • Does not display the last character when text field becoming first responder again.
  • Does not duplicate the existing text when you repeatedly touch the text field.

If you want to use this solution not only with secure text entry, add the isSecureTextEntry check.

class PasswordTextField: UITextField {
    override func becomeFirstResponder() -> Bool {
        let wasFirstResponder = isFirstResponder
        let success = super.becomeFirstResponder()
        if !wasFirstResponder, let text = self.text {
            insertText("\(text)+")
            deleteBackward()
        }
        return success
    }
}



回答15:


Create a subclass of UITextField and override below two methods below:

-(void)setSecureTextEntry:(BOOL)secureTextEntry {
    [super setSecureTextEntry:secureTextEntry];

    if ([self isFirstResponder]) {
        [self becomeFirstResponder];
    }
}

-(BOOL)becomeFirstResponder {
    BOOL became = [super becomeFirstResponder];
    if (became) {
        NSString *originalText = [self text];
        //Triggers UITextField to clear text as first input
        [self deleteBackward];

        //Requires setting text via 'insertText' to fire all associated events
        [self setText:@""];
        [self insertText:originalText];
    }
    return became;
}

Use this class name as your text field class name.




回答16:


I realize this is a little old, but in iOS 6 the UITextField "text" is now by default "Attributed" in Interface Builder. Switching this to be "Plain", which is how it was in iOS 5, fixes this problem.

Also posted this same answer over in the question that @Craig linked.




回答17:


i had the same problem, but got the solution;

-(BOOL)textFieldShouldReturn:(UITextField *)textField
    {

        if(textField==self.m_passwordField)
        {
            text=self.m_passwordField.text;  
        }

        [textField resignFirstResponder];

        if(textField==self.m_passwordField)
        {
            self.m_passwordField.text=text;
        }
        return YES;
    }



回答18:


My solution (until the bug is fixed I suppose) is to subclass UITextField such that it appends to existing text instead of clearing as before (or as in iOS 6). Attempts to preserve original behavior if not running on iOS 7:

@interface ZTTextField : UITextField {
    BOOL _keyboardJustChanged;
}
@property (nonatomic) BOOL keyboardJustChanged;
@end

@implementation ZTTextField
@synthesize keyboardJustChanged = _keyboardJustChanged;

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
        _keyboardJustChanged = NO;
    }
    return self;
}

- (void)insertText:(NSString *)text {
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
    if (self.keyboardJustChanged == YES) {
        BOOL isIOS7 = NO;
        if ([[UIApplication sharedApplication] respondsToSelector:@selector(backgroundRefreshStatus)]) {
            isIOS7 = YES;
        }
        NSString *currentText = [self text];
        // only mess with editing in iOS 7 when the field is masked, wherein our problem lies
        if (isIOS7 == YES && self.secureTextEntry == YES && currentText != nil && [currentText length] > 0) {
            NSString *newText = [currentText stringByAppendingString: text];
            [super insertText: newText];
        } else {
            [super insertText:text];
        }
        // now that we've handled it, set back to NO
        self.keyboardJustChanged = NO;
    } else {
        [super insertText:text];
    }
#else
    [super insertText:text];
#endif
}

- (void)setKeyboardType:(UIKeyboardType)keyboardType {
    [super setKeyboardType:keyboardType];
    [self setKeyboardJustChanged:YES];
}

@end



回答19:


I experimented with answers of dwsolberg and fluidsonic and this seem's to work

override func becomeFirstResponder() -> Bool {

    guard !isFirstResponder else { return true }
    guard super.becomeFirstResponder() else { return false }
    guard self.isSecureTextEntry == true else { return true }
    guard let existingText = self.text else { return true }
    self.deleteBackward() // triggers a delete of all text, does NOT call delegates
    self.insertText(existingText) // does NOT call delegates

    return true
}



回答20:


I needed to adjust @thomas-verbeek solution, by adding a property which deals with the case when user tries to paste any text to the field (text has been duplicated)

class PasswordTextField: UITextField {

    private var barier = true

    override var isSecureTextEntry: Bool {
        didSet {
            if isFirstResponder {
                _ = becomeFirstResponder()
            }
        }
    }

    override func becomeFirstResponder() -> Bool {
        let success = super.becomeFirstResponder()
        if isSecureTextEntry, let text = self.text, barier {
            deleteBackward()
            insertText(text)
        }
        barier = !isSecureTextEntry
        return success
    }

}



回答21:


I used @EmptyStack answer textField.clearsOnBeginEditing = NO; on my password text field passwordTextField.secureTextEntry = YES; but it didn't work out in iOS11 SDK with Xcode 9.3 so I done following code to achieve. Actually I want to keep text(in text field) if user switch between different fields.

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    if (textField.tag == 2) {
        if ([string isEqualToString:@""] && textField.text.length >= 1) {
            textField.text = [textField.text substringToIndex:[textField.text length] - 1];
        } else{
            textField.text = [NSString stringWithFormat:@"%@%@",textField.text,string];
        }
        return false;
    } else {
        return true;
    }
}

I returned false in shouldChangeCharactersInRange and manipulated as i want this code also works if user click on delete button to delete character.




回答22:


when my project upgrade to iOS 12.1 and have this issue. This is my solution about this. It's work okay.



    - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
        NSMutableString *checkString = [textField.text mutableCopy];
        [checkString replaceCharactersInRange:range withString:string];
        textField.text = checkString;
        NSRange selectedRange = NSMakeRange(range.location + string.length, 0);
        UITextPosition* from = [textField positionFromPosition:textField.beginningOfDocument offset:selectedRange.location];
        UITextPosition* to = [textField positionFromPosition:from offset:selectedRange.length];
        textField.selectedTextRange = [textField textRangeFromPosition:from toPosition:to];
        [textField sendActionsForControlEvents:UIControlEventEditingChanged];
        return NO;
    }




回答23:


Thanks to the answers before me. It seems that all that needs to be done is removing and insertion of the text on the same string object right after isSecureTextEntry is set. So I've added the extension below:

extension UITextField {
    func setSecureTextEntry(_ on: Bool, clearOnBeginEditing: Bool = true)   {
        isSecureTextEntry = on

        guard on,
              !clearOnBeginEditing,
              let textCopy = text
        else { return }

        text?.removeAll()
        insertText(textCopy)
    }
}



回答24:


There is another stackoverflow question post: Question

Reading that post it lookes like you need to set the textField.clearsOnBeginEditing = NO in the textFieldShouldBeginEditing delegate method




回答25:


Save the text entered in a property. For example:

NSString *_password;

And then in the delegate:

textFieldDidBeginEditing:(UITextField *)textField
assign textField.text = _password;


来源:https://stackoverflow.com/questions/7305538/uitextfield-with-secure-entry-always-getting-cleared-before-editing

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