web-dev-qa-db-fra.com

La barre d'espace UITextField alignée à droite ne fait pas avancer le curseur dans iOS 7

Dans mon application iPad, j'ai remarqué un comportement différent entre iOS 6 et iOS 7 avec UITextFields.

Je crée UITextField comme suit:

UIButton *theButton = (UIButton*)sender;
UITextField *textField = [[UITextField alloc] initWithFrame:[theButton frame]];

[textField setDelegate:self];
[textField setContentVerticalAlignment:UIControlContentVerticalAlignmentCenter];
[textField setContentHorizontalAlignment:UIControlContentHorizontalAlignmentRight];

textField.textAlignment = UITextAlignmentRight;
textField.keyboardType = UIKeyboardTypeDefault;

...

[textField becomeFirstResponder];

Dans iOS 6, lorsque je tape "hello world", le curseur avance d'un espace lorsque je tape sur la barre d'espace après "hello". 

Sous iOS 7, le curseur n'avance pas lorsque j'appuie sur la barre d'espace. Cependant, lorsque je tape le "w" dans "monde", il montre l'espace et le w.

Comment faire avancer le curseur lorsque la barre d'espace est touchée dans iOS 7?

Mettre à jour:

Si je remplace textField.textAlignment par UITextAlignmentLeft, l’espace apparaît dans iOS 7. Je souhaite le garder aligné si possible.

40
jkh

Ce serait un peu un bidouillage, mais si vous avez vraiment besoin de cela pour ressembler à iOS6, vous pouvez remplacer l'espace par l'espace insécable tel qu'il est écrit. C'est traité différemment. Exemple de code pourrait ressembler à ceci:

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
    // only when adding on the end of textfield && it's a space
    if (range.location == textField.text.length && [string isEqualToString:@" "]) {
        // ignore replacement string and add your own
        textField.text = [textField.text stringByAppendingString:@"\u00a0"];
        return NO;
    }
    // for all other cases, proceed with replacement
    return YES;
}

Si ce n'est pas clair, textField:shouldChangeCharactersInRange:replacementString: est une méthode de protocole UITextFieldDelegate. Ainsi, dans votre exemple, la méthode ci-dessus serait dans le contrôleur de vue désigné par [textField setDelegate:self].

Si vous souhaitez récupérer vos espaces normaux, vous devrez évidemment vous rappeler de reconvertir le texte en remplaçant les occurrences de @"\u00a0" par @" " lors de l'extraction de la chaîne du champ de texte.

41
triazotan

Vous devrez remplacer les espaces normaux par des espaces insécables . Il est préférable de déclencher une action sur un événement de changement pour cela:

  1. Quelque part, ajoutez une action pour l'événement UIControlEventEditingChanged sur votre champ de texte:

    [myTextField addTarget:self action:@selector(replaceNormalSpacesWithNonBreakingSpaces)
                      forControlEvents:UIControlEventEditingChanged];
    
  2. Puis implémentez la méthode replaceNormalSpacesWithNonBreakingSpaces:

    - (void)replaceNormalSpacesWithNonBreakingSpaces
    {
        self.text = [self.text stringByReplacingOccurrencesOfString:@" "
                                                         withString:@"\u00a0"];
    }
    

Cela est plus sûr que d'utiliser textField:shouldChangeCharactersInRange:replacementString:, car si vous renvoyez NO à partir de cette méthode, vous dites en fait que le texte spécifié ne doit pas être modifié. Ainsi, les événements de modification (tels que l'événement IBActions textFieldEditingChanged: ou l'événement UIControlEventEditingChanged de UITextField) ne seront pas déclenchés.

Fixez-le partout:

Si vous souhaitez ce correctif pour tous vos UITextFields, vous pouvez créer une catégorie dans laquelle vous ajoutez ces actions d’événement lorsqu’un UITextField est lancé. Dans l'exemple ci-dessous, je modifie également les espaces insécables à la place d'espaces normaux à la fin de la modification, de sorte que les problèmes éventuels liés aux espaces insécables ne surviennent pas lorsque les données sont utilisées ailleurs. Notez que cet exemple utilise method swizzling donc cela peut paraître un peu bizarre, mais c’est correct.

Le fichier d'en-tête:

//  UITextField+RightAlignedNoSpaceFix.h

#import <UIKit/UIKit.h>

@interface UITextField (RightAlignedNoSpaceFix)
@end

Le fichier d'implémentation:

//  UITextField+RightAlignedNoSpaceFix.m

#import "UITextField+RightAlignedNoSpaceFix.h"

@implementation UITextField (RightAlignedNoSpaceFix)

static NSString *normal_space_string = @" ";
static NSString *non_breaking_space_string = @"\u00a0";

+(void)load
{
    [self overrideSelector:@selector(initWithCoder:)
              withSelector:@selector(initWithCoder_override:)];

    [self overrideSelector:@selector(initWithFrame:)
              withSelector:@selector(initWithFrame_override:)];
}

/**
 * Method swizzles the initWithCoder method and adds the space fix
 * actions.
 */
-(instancetype)initWithCoder_override:(NSCoder*)decoder
{
    self = [self initWithCoder_override:decoder];
    [self addSpaceFixActions];
    return self;
}

/**
 * Method swizzles the initWithFrame method and adds the space fix
 * actions.
 */
-(instancetype)initWithFrame_override:(CGRect)frame
{
    self = [self initWithFrame_override:frame];
    [self addSpaceFixActions];
    return self;
}

/**
 * Will add actions on the text field that will replace normal 
 * spaces with non-breaking spaces, and replaces them back after
 * leaving the textfield.
 *
 * On iOS 7 spaces are not shown if they're not followed by another
 * character in a text field where the text is right aligned. When we
 * use non-breaking spaces this issue doesn't occur.
 *
 * While editing, the normal spaces will be replaced with non-breaking
 * spaces. When editing ends, the non-breaking spaces are replaced with
 * normal spaces again, so that possible problems with non-breaking
 * spaces won't occur when the data is used somewhere else.
 */
- (void)addSpaceFixActions
{

    [self addTarget:self action:@selector(replaceNormalSpacesWithNonBreakingSpaces)
               forControlEvents:UIControlEventEditingDidBegin];

    [self addTarget:self action:@selector(replaceNormalSpacesWithNonBreakingSpaces)
               forControlEvents:UIControlEventEditingChanged];

    [self addTarget:self action:@selector(replaceNonBreakingSpacesWithNormalSpaces)
               forControlEvents:UIControlEventEditingDidEnd];

}

/**
 * Will replace normal spaces with non-breaking spaces.
 */
- (void)replaceNormalSpacesWithNonBreakingSpaces
{
    self.text = [self.text stringByReplacingOccurrencesOfString:normal_space_string
                                                     withString:non_breaking_space_string];
}

/**
 * Will replace non-breaking spaces with normal spaces.
 */
- (void)replaceNonBreakingSpacesWithNormalSpaces
{
    self.text = [self.text stringByReplacingOccurrencesOfString:non_breaking_space_string
                                                     withString:normal_space_string];
}

@end
13
gitaarik

Toutes les réponses ci-dessus sont géniales et très indicatives! Surtout gros merci à meaning-Matters 's réponse ci-dessous . Voici une version testée de Swift 2.0. Rappelez-vous pour affecter le delegate de UITextField à votre ViewController! Bonne codage. 

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

    if (textField == self.desiredTextField) {
        var oldString = textField.text!
        let newRange = oldString.startIndex.advancedBy(range.location)..<oldString.startIndex.advancedBy(range.location + range.length)
        let newString = oldString.stringByReplacingCharactersInRange(newRange, withString: string)
        textField.text = newString.stringByReplacingOccurrencesOfString(" ", withString: "\u{00a0}");
        return false;
    } else {
        return true;
    }

}

-

Et voici Swift 3!

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    if (textField == self.textfield) {
        let oldString = textField.text!
        let newStart = oldString.index(oldString.startIndex, offsetBy: range.location)
        let newEnd = oldString.index(oldString.startIndex, offsetBy: range.location + range.length)
        let newString = oldString.replacingCharacters(in: newStart..<newEnd, with: string)
        textField.text = newString.replacingOccurrences(of: " ", with: "\u{00a0}")
        return false;
    } else {
        return true;
    }
}
11
Jack Song

J'ai mis au point une solution qui sous-classe la classe UITextField et effectue l'échange, sans qu'il soit nécessaire de copier et de coller du code partout. Cela évite également l’utilisation de la méthode Sizzle pour résoudre ce problème.

@implementation CustomTextField

-(id) initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];

    if( self ) {

        [self addSpaceFixActions];
    }

    return self;
}

- (void)addSpaceFixActions {
    [self addTarget:self action:@selector(replaceNormalSpaces) forControlEvents:UIControlEventEditingChanged];
    [self addTarget:self action:@selector(replaceBlankSpaces) forControlEvents:UIControlEventEditingDidEnd];
}


//replace normal spaces with non-breaking spaces.
- (void)replaceNormalSpaces {
    if (self.textAlignment == NSTextAlignmentRight) {
        UITextRange *textRange = self.selectedTextRange;
        self.text = [self.text stringByReplacingOccurrencesOfString:@" " withString:@"\u00a0"];
        [self setSelectedTextRange:textRange];
    }
}

//replace non-breaking spaces with normal spaces.
- (void)replaceBlankSpaces {
    self.text = [self.text stringByReplacingOccurrencesOfString:@"\u00a0" withString:@" "];
}
5
Segsfault

Voici une solution qui fonctionne toujours, y compris pour le collage et l’édition (c’est-à-dire quand vous pouvez ajouter/supprimer des textes avec plusieurs espaces).

- (BOOL)textField:(UITextField*)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString*)string
{
    textField.text = [textField.text stringByReplacingCharactersInRange:range withString:string];
    textField.text = [textField.text stringByReplacingOccurrencesOfString:@" " withString:@"\u00a0"];

    return NO;
}

Ne vous inquiétez pas de la performance de stringByReplacingOccurrencesOfString à chaque fois; les textes dans les interfaces utilisateur sont très très courts par rapport à la vitesse du processeur.

Ensuite, lorsque vous voulez réellement obtenir la valeur du champ de texte:

NSString* text = [textField.text stringByReplacingOccurrencesOfString:@"\u00a0" withString:@" "];

Donc, ceci est bien symétrique.

5
meaning-matters

Transformed triazotan's answer en Swift3.

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

    if (range.location == textField.text?.characters.count && string == " ") {
        let noBreakSpace: Character = "\u{00a0}"
        textField.text = textField.text?.append(noBreakSpace)
        return false
    }
    return true
}
3
Rahul

Vieille question mais toutes les solutions ci-dessus semblent trop compliquées. Voici comment j'ai résolu le problème:

Je suis abonné à deux événements textfield ->

  • TextFieldEditingDidBegin
  • TextFieldEditingEnded

Sur TextFieldEditingDidBegin, je règle simplement textField.textAlignment sur UITextAlignmentLeft . Sur TextFieldEditingEnded, je redonne textField.textAlignment sur UITextAlignmentRight.

Cela a fonctionné parfaitement pour moi et je sens que ce n'est pas un hack. J'espère que ça aide!

2
pnavk

Corrige la suppression de l'espace texte aligné à droite en remplaçant l'espace par un espace insécable

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    if (textField.textAlignment == NSTextAlignmentRight) {
        NSString *text = [textField.text stringByReplacingCharactersInRange:range withString:string];
        textField.text = [text stringByReplacingOccurrencesOfString:@" " withString:@"\u00a0"];

        UITextPosition *startPos = [textField positionFromPosition:textField.beginningOfDocument offset:range.location + string.length];
        UITextRange *textRange = [textField textRangeFromPosition:startPos toPosition:startPos];
        textField.selectedTextRange = textRange;

        return NO;
    }

    return YES;
}

Et vice versa

- (void)textFieldDidEndEditing:(UITextField *)textField
{
    // Replacing non-breaking spaces with spaces and remove obsolete data
    NSString *textString = [[textField.text stringByReplacingOccurrencesOfString:@"\u00a0" withString:@" "] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
    textField.text = textString;
}
1
FunkyKat

Voici Swift 3 de la réponse de @Jack Song

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    if (textField == self.textfield) {
        let oldString = textField.text!
        let newStart = oldString.index(oldString.startIndex, offsetBy: range.location)
        let newEnd = oldString.index(oldString.startIndex, offsetBy: range.location + range.length)
        let newString = oldString.replacingCharacters(in: newStart..<newEnd, with: string)
        textField.text = newString.replacingOccurrences(of: " ", with: "\u{00a0}")
        return false;
    } else {
        return true;
    }
}
1
Matthew Barker

Version Swift 4:

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool{
    if var text = textField.text, range.location == text.count, string == " " {
        let noBreakSpace: Character = "\u{00a0}"
        text.append(noBreakSpace)
        textField.text = text
        return false
    }
    return true
}
1
Wujo

J'ai résolu ce problème dans mon application en utilisant un en utilisant un champ de texte aligné à gauche, puis en utilisant AutoLayout pour aligner tout le champ de texte sur la droite. Ceci simule un champ de texte aligné à droite et gère les espaces de fin sans interférer avec les caractères des espaces, etc.

Le principal obstacle à cette approche est qu'UITextField ne met pas à jour sa taille de contenu intrinsèque lorsque le texte change. Pour contourner ce problème, j'ai sous-classé UITextField afin de calculer automatiquement la taille du contenu intrinsèque à mesure que le texte change. Voici ma sous-classe:

@implementation PLResizingTextField

- (instancetype)init {
    self = [super init];
    if(self) {
        [self addTarget:self action:@selector(invalidateIntrinsicContentSize) forControlEvents:UIControlEventEditingChanged];
    }
    return self;
}

- (CGSize)intrinsicContentSize {
    CGSize size = [super intrinsicContentSize];
    NSString *text = self.text.length ? self.text : self.placeholder;

    CGRect rect = [text boundingRectWithSize:CGSizeMake(CGFLOAT_MAX,CGFLOAT_MAX)
                                     options:NSStringDrawingUsesLineFragmentOrigin
                                  attributes:@{NSFontAttributeName:self.font}
                                     context:nil];
    size.width = CGRectGetWidth(rect);

    return size;
}

@end

Et voici un fragment de mon code de mise en page automatique, utilisant la bibliothèque PureLayout:

[textField autoPinEdgeToSuperviewEdge:ALEdgeTrailing
                            withInset:10];
[textField autoPinEdge:ALEdgeLeading
                toEdge:ALEdgeTrailing
                ofView:cell.textLabel
            withOffset:10
              relation:NSLayoutRelationGreaterThanOrEqual];
[textField setContentHuggingPriority:UILayoutPriorityDefaultHigh
                             forAxis:UILayoutConstraintAxisHorizontal];

Points importants à noter ici:

  1. définir le contenu prioritaire dans le champ de texte
  2. utilisez une relation NSLayoutRelationGreaterThanOrEqual entre le bord gauche du champ de texte et la vue située à gauche de celui-ci (ou le bord gauche de Superview).
0
James

Ma solution suivante prend également en charge le problème avec le curseur qui saute à la fin lorsque vous tapez un espace au milieu ou au début de la chaîne. De plus, coller une chaîne est maintenant traité correctement aussi.

Je mets également un chèque pour les champs d'adresse email et autres chèques, mais la partie intéressante est la dernière partie. Cela fonctionne parfaitement pour moi, je n'ai pas encore trouvé de problème avec cela. 

Vous pouvez directement copier/coller ceci dans votre projet. N'oubliez pas de mettre en œuvre les fonctions didBeginEditing et didEndEditing pour remplacer les espaces par des espaces insécables et inversement!

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    if (textField.textAlignment != NSTextAlignmentRight) //the whole issue only applies to right aligned text
        return YES;

    if (!([string isEqualToString:@" "] || string.length > 1)) //string needs to be a space or paste action (>1) to get special treatment
        return YES;

    if (textField.keyboardType == UIKeyboardTypeEmailAddress) //keep out spaces from email address field
    {
        if (string.length == 1)
            return NO;
        //remove spaces and nonbreaking spaces from paste action in email field:
        string = [string stringByReplacingOccurrencesOfString:@" " withString:@""];
        string = [string stringByReplacingOccurrencesOfString:@"\u00a0" withString:@""];
    }

    //special treatment starts here
    string = [string stringByReplacingOccurrencesOfString:@" " withString:@"\u00a0"];
    UITextPosition *beginning = textField.beginningOfDocument;
    textField.text = [textField.text stringByReplacingCharactersInRange:range withString:string];
    UITextPosition *start = [textField positionFromPosition:beginning offset:range.location+string.length];
    UITextPosition *end = [textField positionFromPosition:start offset:range.length];
    UITextRange *textRange = [textField textRangeFromPosition:start toPosition:end];
    [textField setSelectedTextRange:textRange];

    return NO;
}
0
Janneman

Swift4

//
//  UITextFiled+fixspace.Swift
//  anbaobao
//
//  Created by 荆文征 on 2018/11/9.
//  Copyright © 2018 com.baimaodai. All rights reserved.
//

import UIKit

extension UITextField {
    /// Runtime 键
    private struct AssociatedKeys {

        static var toggleState: UInt8 = 0
    }

    /// 是否已经修复 右侧问题
    private var isFixedRightSpace: Bool {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.toggleState) as? Bool ?? false
        }
        set(newValue) {
            objc_setAssociatedObject(self, &AssociatedKeys.toggleState, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

    open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

        if self.textAlignment == .right && !isFixedRightSpace {
            self.isFixedRightSpace = true
            self.addTarget(self, action: #selector(replaceNormalSpacesWithNonBreakingSpaces(textFiled:)), for: UIControl.Event.editingChanged)
        }

        return super.hitTest(point, with: event)
    }

    @objc func replaceNormalSpacesWithNonBreakingSpaces(textFiled: UITextField) {

        if textFiled.markedTextRange == nil && textFiled.text?.contains(find: " ") ?? false {

            let editRange = selectedTextRange
            textFiled.text = textFiled.text?.replacingOccurrences(of: " ", with: "\u{00a0}")
            selectedTextRange = editRange
        }
    }
}
0
Z King

J'ai utilisé la réponse de Jack Song pour Swift 2 pendant un certain temps, jusqu'à ce que je réalise que les espaces non-freins posent des problèmes lorsqu'ils sont restitués en HTML ailleurs, de même que les sauts de ligne deviennent désordonnés dans UITextView. J'ai donc amélioré la solution pour que les caractères sans crochets soient nettoyés immédiatement.

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    if (textField == self.desiredTextField) {
       var oldString = textView.text!
       oldString = oldString.stringByReplacingOccurrencesOfString("\u{00a0}", withString: " ");
       let newRange = oldString.startIndex.advancedBy(range.location)..<oldString.startIndex.advancedBy(range.location + range.length)
       let alteredText = text.stringByReplacingOccurrencesOfString(" ", withString: "\u{00a0}")
       textView.text = oldString.stringByReplacingCharactersInRange(newRange, withString: alteredText)
       return false;
    } else {
       return true;
    }
}
0
Vitalii