Reputation: 6475
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?
Upvotes: 59
Views: 60592
Reputation: 2117
@Thomas Verbeek solution works great for UIKit but for SwiftUI and SecureField
I had to use a bit of swizzling to make it work:
extension UITextField {
@objc func swizzled_becomeFirstResponder() -> Bool {
let success = swizzled_becomeFirstResponder()
if isSecureTextEntry, let text = self.text {
self.text?.removeAll()
insertText(text)
}
return success
}
static func swizzleBecomeFirstResponder() {
let originalSelector = #selector(becomeFirstResponder)
let swizzledSelector = #selector(swizzled_becomeFirstResponder)
guard let originalMethod = class_getInstanceMethod(self, originalSelector),
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector) else {
return
}
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
And in app delegate:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UITextField.swizzleBecomeFirstResponder()
return true
}
Make sure to only call UITextField.swizzleBecomeFirstResponder()
once in the app's lifetime.
Upvotes: 0
Reputation: 9356
This version is inspired by Iaenhaal's. Adding the extra character and calling DeleteBackward()
prevents the latest character from being displayed.
public override bool BecomeFirstResponder()
{
var result = base.BecomeFirstResponder();
if (SecureTextEntry && !string.IsNullOrEmpty(Text))
{
var text = Text;
Text = string.Empty;
InsertText($"{text} ");
DeleteBackward();
}
return result;
}
Upvotes: 0
Reputation: 1263
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
}
}
Upvotes: 0
Reputation: 8215
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
}
}
Upvotes: 1
Reputation: 2837
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
}
Upvotes: 0
Reputation: 6021
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
}
}
Upvotes: 15
Reputation: 11
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)
}
}
Upvotes: 0
Reputation: 427
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.
Upvotes: 1
Reputation: 512
IOS 12, Swift 4
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
}
}
Upvotes: 6
Reputation: 219
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;
}
Upvotes: 0
Reputation: 3291
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.
Upvotes: 0
Reputation: 1103
@Eric's answer works but I had two issues with it.
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;
}
Upvotes: 22
Reputation: 1363
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
}
}
Upvotes: 0
Reputation: 2431
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!
Upvotes: 38
Reputation: 2043
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.
Upvotes: 50
Reputation: 493
@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!
Upvotes: 6
Reputation: 338
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
}
Upvotes: 1
Reputation: 1
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
}
Upvotes: 0
Reputation: 4676
We solved it based on dwsolberg's answer with two fixes:
deleteBackward
and insertText
cause the value changed event to be firedSo 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
}
}
Upvotes: 1
Reputation: 979
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.
Upvotes: 5
Reputation: 4437
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)
}
}
Upvotes: 1
Reputation: 1305
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.
Upvotes: 6
Reputation: 10096
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;
}
Upvotes: 1
Reputation: 1747
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
Upvotes: 0
Reputation: 562
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;
}
Upvotes: 0
Reputation: 1892
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.
Upvotes: 0
Reputation: 785
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.
Upvotes: 5
Reputation: 51374
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.
Upvotes: 65
Reputation: 3197
Save the text entered in a property. For example:
NSString *_password;
And then in the delegate:
textFieldDidBeginEditing:(UITextField *)textField
assign textField.text = _password;
Upvotes: -2
Reputation: 5399
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
Upvotes: -1