web-dev-qa-db-fra.com

UITextField avec saisie sécurisée, toujours effacée avant édition

J'ai un problème étrange dans lequel mon UITextField qui contient une entrée sécurisée est toujours effacé lorsque je tente de le modifier. J'ai ajouté 3 caractères au champ, puis un autre champ est retourné, le curseur est en 4ème position, mais lorsque j'essaie d'ajouter un autre caractère, le texte entier du champ est effacé par le nouveau caractère. J'ai «efface lorsque le montage commence» décoché dans la plume. Alors, quel serait le problème? Si je supprime la propriété d'entrée sécurisée, tout fonctionne correctement, est-ce la propriété des champs de texte d'entrée sécurisée? Y a-t-il un moyen d'empêcher ce comportement?

29
Nithin

Ensemble, 

textField.clearsOnBeginEditing = NO;

Remarque: cela ne fonctionnera pas si secureTextEntry = YES . Il semble que, par défaut, iOS efface le texte des champs de texte de saisie sécurisée avant édition, peu importe/ clearsOnBeginEditing est YES ou NO.

42
EmptyStack

Si vous ne souhaitez pas que le champ soit effacé, même lorsque secureTextEntry = YES, utilisez:

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

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

    textField.text = updatedString;

    return NO;
}

J'ai rencontré un problème similaire lors de l'ajout de la fonctionnalité d'affichage du texte du mot de passe/masquage à une vue d'inscription.

39
Eric

Si vous utilisez Swift 3 , essayez cette sous-classe.

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
    }

}

Pourquoi ça marche?

TL; DR: si vous modifiez le champ en basculant isSecureTextEntry, assurez-vous d’appeler becomeFirstResponder.


Changer la valeur de isSecureTextEntry fonctionne correctement jusqu'à ce que l'utilisateur modifie le champ de texte - le champ de texte est effacé avant de placer le ou les nouveaux caractères. Cette libération provisoire semble avoir lieu pendant l'appel becomeFirstResponder de UITextField. Si cet appel est combiné à l'astuce deleteBackwardinsertText (comme le démontrent les réponses de _/@Aleksey et @dwsolberg), le texte saisi est conservé, annulant apparemment l'effacement provisoire.

Cependant, lorsque la valeur de isSecureTextEntry change alors que le champ de texte est le premier répondant (par exemple, l'utilisateur tape son mot de passe, bascule un bouton "Afficher le mot de passe" dans les deux sens, puis continue à taper), le champ de texte sera réinitialisé normalement.

Pour conserver le texte d'entrée dans ce scénario, cette sous-classe déclenche becomeFirstResponder uniquement si le champ de texte était le premier répondant. Cette étape semble manquer dans d'autres réponses.

Merci @Patrick Ridd pour la correction!

16
Thomas Verbeek

La réponse de @ Eric fonctionne, mais elle me posait deux problèmes.

  1. Comme @malex l'a fait remarquer, tout changement de texte au milieu placera un chariot à la fin du texte.
  2. J'utilise rac_textSignal de ReactiveCocoa et modifier le texte directement ne déclencherait pas le signal.

Mon code final

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

Ajout de Swift3 par @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;
}
14
Ahmad Baraka

J'avais un problème similaire. J'ai les champs de texte de connexion (secureEntry = NO) et de mot de passe (secureEntry = YES) incorporés dans une vue de tableau. J'ai essayé de mettre

textField.clearsOnBeginEditing = NO;

dans les deux méthodes déléguées pertinentes (textFieldDidBeginEditing et textFieldShouldBeginEditing), qui ne fonctionnaient pas. Après être passé du champ mot de passe au champ de connexion, tout le champ de connexion serait effacé si je tentais de supprimer un seul caractère. J'ai utilisé le textFieldShouldChangeCharactersInRange pour résoudre mon problème, comme suit:

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

La valeur range.length == 1 renvoie true lorsque l'utilisateur entre un retour arrière, ce qui est (étrangement) le seul moment où je verrais le champ effacé.

5
Ampers4nd

J'ai essayé toutes les solutions ici et là et je suis finalement arrivé avec cette priorité:

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

Il déclenche l'effacement de UITextField, puis restaure le texte original. 

5
Aleksey

La réponse de Thomas Verbeek m'a beaucoup aidé:

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
    }

}

Sauf que j'ai trouvé un bug dans mon code avec. Après avoir implémenté son code, si vous avez du texte dans textField et que vous tapez sur la zone textField, il ne supprimera que le premier caractère, puis réinsérera tout le texte. Fondamentalement, coller à nouveau le texte. 

Pour remédier à cela, j'ai remplacé la deleteBackward() par une self.text?.removeAll() et cela a fonctionné à merveille. 

Je n'aurais pas pu aller aussi loin sans la solution originale de Thomas, alors merci Thomas!

5
Patrick Ridd

Swift 3/Swift 4

yourtextfield.clearsOnInsertion = false
yourtextfield.clearsOnBeginEditing = false

Remarque: cela ne fonctionnera pas si secureTextEntry = YES. Il semble que, par défaut, iOS efface le texte des champs de texte de saisie sécurisée avant la modification, peu importe que clearsOnBeginEditing ait la valeur YES ou NO.

Méthode d'utilisation facile et son travail 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
    }

}
4
Shakeel Ahmed

Basé sur la solution d'Aleksey, j'utilise cette sous-classe.

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

Changer les caractères de remplacement dans la plage ne fonctionne pas pour moi car je fais parfois d'autres choses avec cela, et ajouter plus le rend plus susceptible d'être bogué.

C'est bien parce que ça marche à peu près parfaitement. La seule chose étrange est que le dernier caractère est à nouveau affiché lorsque vous appuyez de nouveau sur le champ. En fait, j'aime bien cela parce que cela agit comme une sorte d'espace réservé.

2
dwsolberg

Après avoir joué avec la solution de @malex, je suis arrivé à cette version Swift :

1) N'oubliez pas de transformer votre contrôleur de vue en UITextFieldDelegate:

class LoginViewController: UIViewController, UITextFieldDelegate {
...
}

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

2) utiliser ceci:

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
}

... et si vous utilisez ceci pour la fonctionnalité "afficher/masquer le mot de passe", vous devrez probablement sauvegarder et restaurer la position du curseur lorsque vous activez/désactivez secureTextEntry. Voici comment (faites-le à l'intérieur de la méthode de commutation):

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

C'est le code Swift de mon projet qui est testé et traite les modifications de retour arrière et d'entrée sécurisée false/true

// obtient la saisie de l'utilisateur et appelle les méthodes de validation

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

Nous l'avons résolu en se basant sur la réponse de dwsolberg avec deux corrections:

  • le texte du mot de passe a été dupliqué lors de la saisie dans un champ de mot de passe déjà sélectionné
  • le dernier caractère du mot de passe a été révélé lors de la saisie dans le champ du mot de passe
  • deleteBackward et insertText provoquent le déclenchement de l'événement de valeur modifiée

Nous avons donc proposé ceci (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
    }
}
1
fluidsonic

Si vous souhaitez utiliser secureTextEntry = YES et un comportement visuel correct pour le transport, vous avez besoin de ceci:

- (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;
}
1
malex

Ma solution (jusqu'à ce que le bogue soit corrigé, je suppose) consiste à sous-classer UITextField de telle sorte qu'il soit ajouté au texte existant au lieu d'être effacé comme auparavant (ou comme dans iOS 6). Tente de conserver le comportement d'origine si non exécuté sur 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
0
Billy Gray

Je réalise que c'est un peu vieux, mais dans iOS 6, le "texte" UITextField est maintenant par défaut "Attribué" dans Interface Builder. Le fait de changer cela en "simple", comme c'était le cas dans iOS 5, corrige ce problème. 

A également posté cette même réponse dans la question que @Craig a liée. 

0
Matt S.

IOS 12, Swift 4

  • N'affiche pas le dernier caractère lorsque le champ de texte redevient le premier répondant.
  • Ne duplique pas le texte existant lorsque vous touchez plusieurs fois le champ de texte.

Si vous souhaitez utiliser cette solution non seulement avec la saisie de texte sécurisée, ajoutez la vérification isSecureTextEntry.

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
    }
}
0
N. Belokrinitsky

lors de la mise à niveau de mon projet vers iOS 12.1 et que ce problème existe. Ceci est ma solution à ce sujet. Ça marche bien.



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

0
Ace

J'ai expérimenté des réponses de dwsolberg et de fluidsonic et cela semble fonctionner 

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

J'ai utilisé @EmptyStack answer textField.clearsOnBeginEditing = NO; dans le champ de saisie passwordTextField.secureTextEntry = YES; de mon mot de passe, mais cela n'a pas fonctionné dans le SDK iOS11 avec Xcode 9.3. En fait, je souhaite conserver du texte (dans un champ de texte) si l'utilisateur bascule entre différents champs. 

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

J'ai retourné false dans shouldChangeCharactersInRange et manipulé comme je le souhaite ce code fonctionne également si l'utilisateur clique sur le bouton Supprimer pour supprimer le caractère.

0
Aleem

j'ai eu le même problème, mais j'ai eu la 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;
    }
0
Superdev

J'avais besoin d'ajuster la solution @ thomas-verbeek en ajoutant une propriété qui traite le cas lorsque l'utilisateur tente de coller du texte dans le champ (le texte a été dupliqué)

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
    }

}
0
olejnjak