UITextField with secure entry, always getting cleared before editing

IphoneUitextfield

Iphone Problem Overview


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?

Iphone Solutions


Solution 1 - Iphone

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.

Solution 2 - Iphone

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.

Solution 3 - Iphone

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!

Solution 4 - Iphone

@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;
}

Solution 5 - Iphone

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
    }
}

Solution 6 - Iphone

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.

Solution 7 - Iphone

@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!

Solution 8 - Iphone

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.

Solution 9 - Iphone

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.

Solution 10 - Iphone

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
    }
}

Solution 11 - Iphone

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;
}

Solution 12 - Iphone

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)
       }
}

Solution 13 - Iphone

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
	}
}

Solution 14 - Iphone

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
}

Solution 15 - Iphone

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.

Solution 16 - Iphone

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.

Solution 17 - Iphone

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;
    }

Solution 18 - Iphone

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

Solution 19 - Iphone

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
}

Solution 20 - Iphone

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
    }

}

Solution 21 - Iphone

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.

Solution 22 - Iphone

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;
}


Solution 23 - Iphone

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)
    }
}

Solution 24 - Iphone

Swift5 version of https://stackoverflow.com/a/29195723/1979953.

func textField(
    _ textField: UITextField,
    shouldChangeCharactersIn range: NSRange,
    replacementString string: String
) -> Bool {
    let 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 = NSRange(
        location: range.location + string.count,
        length: 0
    )

    guard let from = textField.position(
        from: textField.beginningOfDocument,
        offset: selectedRange.location
    ) else {
        assertionFailure()
        return false
    }

    guard let to = textField.position(
        from: from,
        offset: selectedRange.length
    ) else {
        assertionFailure()
        return false
    }

    textField.selectedTextRange = textField.textRange(
        from: from,
        to: to
    )

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

    return false
}

Solution 25 - Iphone

The solutions posted didn't work for me, I ended up with this one:

import UIKit

final class SecureTextFieldFixed: UITextField {
    override var isSecureTextEntry: Bool {
        didSet {
            if isFirstResponder {
                _ = becomeFirstResponder()
            }
        }
    }

    override func becomeFirstResponder() -> Bool {
        let success = super.becomeFirstResponder()
        if isSecureTextEntry, let enteredText = text {
            text?.removeAll()
            insertText(enteredText)
        }
        return success
    }
}

Solution 26 - Iphone

I have modified Ahmed's answer for swift 5.

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    if textField.isEqual(txtFieldPassword) {
    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 as NSString).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: UIControl.Event.editingChanged)

        return false;
    } else {
        return true
    }
    
}

Solution 27 - Iphone

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

Solution 28 - Iphone

Save the text entered in a property. For example:

NSString *_password;

And then in the delegate:

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionNithinView Question on Stackoverflow
Solution 1 - IphoneEmptyStackView Answer on Stackoverflow
Solution 2 - IphoneEricView Answer on Stackoverflow
Solution 3 - IphoneThomas VerbeekView Answer on Stackoverflow
Solution 4 - IphoneAhmad BarakaView Answer on Stackoverflow
Solution 5 - IphoneShakeel AhmedView Answer on Stackoverflow
Solution 6 - IphoneAlekseyView Answer on Stackoverflow
Solution 7 - IphonePatrick RiddView Answer on Stackoverflow
Solution 8 - IphoneAmpers4ndView Answer on Stackoverflow
Solution 9 - IphonedwsolbergView Answer on Stackoverflow
Solution 10 - IphoneIaenhaallView Answer on Stackoverflow
Solution 11 - IphonemalexView Answer on Stackoverflow
Solution 12 - IphoneVitaliiView Answer on Stackoverflow
Solution 13 - IphonefluidsonicView Answer on Stackoverflow
Solution 14 - IphoneovidiurView Answer on Stackoverflow
Solution 15 - IphoneMilan AgarwalView Answer on Stackoverflow
Solution 16 - IphoneMatt S.View Answer on Stackoverflow
Solution 17 - IphoneSuperdevView Answer on Stackoverflow
Solution 18 - IphoneBilly GrayView Answer on Stackoverflow
Solution 19 - IphoneIvanView Answer on Stackoverflow
Solution 20 - IphoneolejnjakView Answer on Stackoverflow
Solution 21 - IphoneAleemView Answer on Stackoverflow
Solution 22 - IphoneAceView Answer on Stackoverflow
Solution 23 - Iphoneredmage1993View Answer on Stackoverflow
Solution 24 - Iphoneshingo.nakanishiView Answer on Stackoverflow
Solution 25 - IphoneRichard TopchiiView Answer on Stackoverflow
Solution 26 - IphoneNarasimha NallamsettyView Answer on Stackoverflow
Solution 27 - IphoneCraig MellonView Answer on Stackoverflow
Solution 28 - IphoneLirikView Answer on Stackoverflow