UITextField with protected record, always cleared before editing
I have a weird problem whereby mine UITextField
, which contains a protected entry, is always cleared when I try to edit it. I added 3 characters
to the field, went to another field and came back, the cursor is at the 4th position character
, but when I try to add another character, all the text in the field is cleared with the new character. I have "Clear When Editing Begins" removed in pen. So what's the problem? If I remove the Protected Record property everything works fine, so is this the Protected Record property textfields
? Is there a way to prevent this behavior?
source to share
If you don't want the field to be cleared even if 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 ran into a similar problem when adding a show / hide password text function as a registration.
source to share
If you are using Swift 3 then drop this subclass.
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 it work?
TL; DR: if you edit a field when toggling isSecureTextEntry
, make sure you call becomeFirstResponder
.
The value toggle isSecureTextEntry
works fine as long as the user is not editing the textbox - the textbox is cleared before placing the new character (s). This pre-cleanup appears to happen during the call becomeFirstResponder
UITextField
. If this call is combined with the deleteBackward
/ trick insertText
(as shown in the answers by @Aleksey and @dwsolberg), the input text is preserved, seemingly canceling the precleaning.
However, when the value isSecureTextEntry
changes when the textbox is the first responder (for example, the user enters their password, toggles the "show password" button back and forth, then continues typing), the textbox will be reset as usual.
To keep the input text in this script, this subclass becomeFirstResponder
only runs if the textbox was the first responder. This step seems to be missing from other answers.
Thanks @Patrick Ridd for the fix!
source to share
@Erik works, but I had two problems.
- As @malex pointed out, any change to the text in the middle will contain a caret at the end of the text.
- I am using rac_textSignal from ReactiveCocoa and changing the text will not directly trigger a signal.
My last 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;
}
Adding Swift3 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;
}
source to share
@ 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
}
}
Also, I found a bug in my code. After implementing his code, if you have text in the textfield and you click on the textField, it will only remove the first char and then re-insert all the text. Basically pasting text again.
To fix this, I replaced deleteBackward()
with self.text?.removeAll()
and it worked like a charm.
I wouldn't get that far from Thomas's original solution, so thanks to Thomas!
source to share
I had a similar problem. I have a login (secureEntry = NO) and password (secureEntry = YES) embedded in the table view. I tried to install
textField.clearsOnBeginEditing = NO;
inside both of the respective delegate methods (textFieldDidBeginEditing and textFieldShouldBeginEditing) that don't work. After moving from the password field to the login field, the entire input field is cleared if I try to delete one character. I used textFieldShouldChangeCharactersInRange to solve my problem like this:
-(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;
}
}
Range.length == 1 returns true when the user enters backspace, which is (oddly enough) the only time I see the field cleared.
source to share
I tried all the solutions here and there and finally came up with this override:
- (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 launches the UITextField and then restores the original text.
source to share
Swift 3 / Swift 4
yourtextfield.clearsOnInsertion = false
yourtextfield.clearsOnBeginEditing = false
Note. This will not work if secureTextEntry = YES. Apparently, by default, iOS clears the text of safe input text boxes before editing, regardless of whether the OnBeginEditing is cleared YES or NO.
Easy way to use and it works 100%
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
}
}
source to share
Based on Alexey's solution, I am 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 character substitution in the range doesn't work for me because sometimes I do something about it and adding a larger number makes it more likely.
This is good because it works great. The only oddity is that the last character is displayed again when you step back into the field. I really like this because it acts like a kind of placeholder.
source to share
If you want to use secureTextEntry = YES and the correct visual behavior for transport, you need the following:
- (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;
}
source to share
After playing around with the solution from @malex I ended up with this version of Swift :
1) Don't forget to make your view controller 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 do this for the show / hide password function, you will most likely need to save and restore the carriage position when you enable / disable secureTextEntry. Here's how to do it inside the switch 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)
}
}
source to share
We solved this based on dwsolberg's answer with two fixes:
- the password text is duplicated when clicking on the already focused password field
- the last character of the password was found when clicking in the password field
-
deleteBackward
andinsertText
raises an event with a changed value
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
}
}
source to share
This is a quick code from my project that has been tested and deals with backspace and secure-entry false / true changes
// get user input and call 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
}
source to share
IOS 12, Swift 4
- Doesn't display the last character when the text box becomes first responder again.
- Does not duplicate existing text when tapping the text box again.
If you want to use this solution for more than just 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
}
}
source to share
Subclass UITextField and override the following two methods:
-(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 the class name for the text box.
source to share
I had the same problem but got a 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;
}
source to share
My solution (until the bug is fixed, I suppose) is to subclass UITextField in such a way that it is appended to the existing text instead of being cleared as before (or as in iOS 6). Attempts to keep the original behavior when 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
source to share
I have experimented with dwsolberg and fluidsonic answers and this seems 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
}
source to share
I needed to customize @ thomas-verbeek's solution by adding a property that deals with the case when the user tries to paste any text into the field (text is 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
}
}
source to share
I used @EmptyStack's answer textField.clearsOnBeginEditing = NO;
in my password textbox passwordTextField.secureTextEntry = YES;
but it didn't work in iOS11 SDK with Xcode 9.3, so I made the following code to achieve. Actually I want to keep the text (in the textbox) if the user switches 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 to also work if the user clicks the delete button to delete the character.
source to share
when my project upgrades to iOS 12.1 and this issue occurs. This is my solution for this. It works well.
- (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;
}
source to share
Thanks for the answers before me. It seems that all one has to do is delete and paste the text into the same string object immediately after setting isSecureTextEntry. So I 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)
}
}
source to share