Right aligned UITextField spacebar does not advance cursor in iOS 7
Asked Answered
K

16

45

In my iPad app, I noticed different behavior between iOS 6 and iOS 7 with UITextFields.

I create the UITextField as follows:

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

In iOS 6, when I type "hello world" the cursor advances a blank space when I hit the spacebar after "hello."

In iOS 7, the cursor does not advance when I hit the spacebar. However, when I type the "w" in "world," it shows the space and the w.

How can I advance the cursor when the spacebar is hit in iOS 7?

Update:

If I change the textField.textAlignment to UITextAlignmentLeft, then the space appears in iOS 7. I would like to keep it right aligned, if possible.

Krafftebing answered 24/10, 2013 at 15:7 Comment(0)
O
49

It would be a bit of a hack, but if you really need that to look the iOS6 way, you can replace space with non-breaking space as it's written. It's treated differently. Example code could look like this:

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

In case it's not clear, textField:shouldChangeCharactersInRange:replacementString: is a UITextFieldDelegate protocol method, so in your example, the above method would be in the viewcontroller designated by [textField setDelegate:self].

If you want your regular spaces back, you will obviously also need to remember to convert the text back by replacing occurrences of @"\u00a0" with @" " when getting the string out of the textfield.

Outmost answered 21/11, 2013 at 18:49 Comment(6)
This only work for adding/deleting a character at a time; so not when pasting or deleting text with multiple spaces in them. And this can be done a bit simpler; see my answer.Calendar
If you return NO from textField:shouldChangeCharactersInRange:replacementString: you could break things. Please see my answer for a safer method.Solis
not working properly for me, I have to tap spacebar two times to start adding space at the end of string and same happens in case of backspace button to delete last space characters.Embolism
Can you explain why Apple is doing it this way in iOS7+? I'm seeing it in 8.1 now so I assume it's not a bug. Is there a UI rationale behind the change that we should consider not circumventing?Statecraft
I have no official information on this, but IMO it's easy to guess. UITextFields most common usecase is probably various kinds of forms to input data. It's extremely rare to explicitly desire data with trailing spaces in it. So it's probably a silent built-in idiot-proofing measure.Outmost
Then why is it allowed in left aligned fields but not in right aligned? This makes no senseIodate
L
14

All the answers above are awesome and very indicative! Especially big thanks to meaning-matters's answer below. Here's a tested Swift 2.0 version. Remember to assign the delegate of the UITextField to your ViewController! Happy coding.

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

}

--

And here is 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;
    }
}
Lenticel answered 5/3, 2016 at 5:30 Comment(1)
This crashes if the user inserts more than one emoji. Any way to fix it?Senskell
S
13

You'll have to replace the normal spaces with non-breaking spaces. It's best to trigger an action on a change event for this:

  1. Somewhere add an action for the UIControlEventEditingChanged event on your textfield:

    [myTextField addTarget:self action:@selector(replaceNormalSpacesWithNonBreakingSpaces)
                      forControlEvents:UIControlEventEditingChanged];
    
  2. Then implement the replaceNormalSpacesWithNonBreakingSpaces method:

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

This is safer than using textField:shouldChangeCharactersInRange:replacementString:, because if you return NO from this method, you're actually saying that the specified text should not be changed. This will cause change events (like the IBActions textFieldEditingChanged: or the UITextField's UIControlEventEditingChanged event) to not be triggered.

Fix it everywhere:

If you want this fix for all your UITextFields you can create a category where you add these event actions when a UITextField is initiated. In the example below I also change the non-breaking spaces back to normal spaces when editing ended, so that possible problems with the non-breaking spaces won't occur when the data used somewhere else. Note that this example uses method swizzling so it might look a bit weird, but it's correct.

The header file:

//  UITextField+RightAlignedNoSpaceFix.h

#import <UIKit/UIKit.h>

@interface UITextField (RightAlignedNoSpaceFix)
@end

The implementation file:

//  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
Solis answered 19/3, 2014 at 16:26 Comment(1)
I like the use of target/action but it interferes when editing the text (for example deleting a letter in the middle of the string causes the caret to jump to the end of the string)Cima
C
5

Here's a solution that always works, also for pasting and editing (i.e. when you may add/delete texts with multiple spaces).

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

Don't worry about performance of doing stringByReplacingOccurrencesOfString every time; texts in UIs are very very short relative to CPU speed.

Then when you actually want to get the value from the text field:

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

So this is a nicely symmetrical.

Calendar answered 5/3, 2014 at 22:44 Comment(1)
Returning NO in -textField:shouldChangeCharactersInRange:replacementString suppres UITextFieldTextDidChangeNotification. So you can send it in your method [[NSNotificationCenter defaultCenter] postNotificationName:UITextFieldTextDidChangeNotification object:textField]; to return default behaviourErastes
L
5

I've came up with a solution that subclasses the UITextField class and performs the swap, without the need of copying and pasting code everywhere. This also avoids using method sizzle to fix this.

@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:@" "];
}
Liebfraumilch answered 21/1, 2015 at 21:22 Comment(0)
F
4

Transformed triazotan's answer into 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
}
Fichte answered 24/1, 2017 at 9:41 Comment(0)
U
4

Swift 4 version:

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
}
Upsurge answered 22/6, 2018 at 13:35 Comment(1)
Perfect solution for my case!Vortex
H
3

Old question but all the above solutions seem overly complicated. Here is how I solved the issue:

I subscribed to two textfield events ->

  • TextFieldEditingDidBegin
  • TextFieldEditingEnded

On TextFieldEditingDidBegin, I simple set textField.textAlignment to UITextAlignmentLeft. On TextFieldEditingEnded, I set textField.textAlignment back to UITextAlignmentRight.

This worked flawlessly for me and I feel like its not a hack. Hope it helps!

Hargett answered 5/9, 2014 at 20:23 Comment(1)
This is deprecated over ios 6.0. textField.textAlignment = UITextAlignmentLeft;Desai
A
2

These solutions do not work for iOS 15 and up. To clarify - solutions using non-breaking white space combined with UITextField delegate.

Alvardo answered 7/10, 2022 at 14:34 Comment(2)
so how do we resolve this ios15*?Hockey
@Hockey I have opened a bug report with Apple trough feedback assistant but its not fixed yet. Maybe you can open one as wellAlvardo
E
1

Fix right aligned text space removing by replacing space with non-breaking space

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

And 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;
}
Exuberate answered 2/12, 2014 at 15:52 Comment(0)
L
1

I solved this issue in my app using an by using a left-aligned text field, and then used AutoLayout to align the entire text field to the right. This simulates a right-aligned text field and handles trailing spaces without messing around with space characters etc.

The main hurdle in this approach is that UITextField does not update it's intrinsic content size as the text changes. To get around this I subclassed UITextField to automatically calculate intrinsic content size as the text changes. Here's my subclass:

@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

And here's a fragment of my auto layout code, using the PureLayout library:

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

Important points to note here:

  1. set content hugging priority on the text field
  2. use a NSLayoutRelationGreaterThanOrEqual relation between the left edge of text field and the view to the left of it (or superview's left edge).
Legate answered 9/6, 2015 at 23:52 Comment(1)
This is the best solution, thank you!Knowledgeable
A
1

Here is Swift 3 from @Jack Song's answer

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;
    }
}
Aloe answered 2/2, 2017 at 18:58 Comment(0)
L
1
extension UITextField {

    /// runtime key
    private struct AssociatedKeys {

        ///
        static var toggleState: UInt8 = 0
    }

    /// prevent multiple fix
    private var isFixedRightSpace: Bool {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.toggleState) as? Bool ?? false
        }
        set {
            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)
    }

    /// replace space to \u{00a0}
    @objc private func replaceNormalSpacesWithNonBreakingSpaces(textFiled: UITextField) {

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

            /// keep current range
            let editRange = selectedTextRange
            textFiled.text = textFiled.text?.replacingOccurrences(of: " ", with: "\u{00a0}")
            /// reset this range
            selectedTextRange = editRange
        }
    }
}

Lithiasis answered 5/12, 2018 at 7:11 Comment(0)
F
1

The solution which replaced the invisible trailing spaces with non-breaking spaces stopped working in iOS 15 and later. As this issue has been driving me nuts in my app for over a year now, I tried to find a solution which still works in the newer systems. I think I succeeded, but it is even hackier than the previous one but it does the trick. The main idea is to put an additional Syriac Abbreviation Mark (code point 0x070f) to the end if the text ends with a whitespace. This special character tricks the text system into showing the space but is not visible itself in this state. I tested this solution with iOS 15, 16 and 17 and it works fine there. There is no guarantee however that this will work forever with future systems. The following code needs to put in the UITextFieldDelegate like in the other solutions.

static let trailingSpaceSuffix = "\u{070f}"


func textField(_ textField: UITextField,
               shouldChangeCharactersIn range: NSRange,
               replacementString: String) -> Bool {
    var text = textField.text ?? ""
    var range = range
    if range.location > 0, range.location == text.utf16.count - 1, range.length > 0,
       text.hasSuffix(Self.trailingSpaceSuffix) {
        range.location -= 1
        range.length   += 1
    }
    text = (text as NSString).replacingCharacters(in: range, with: replacementString)
    text = text.replacingOccurrences(of: Self.trailingSpaceSuffix, with: "")
    if let last = text.last, last.isWhitespace { text += Self.trailingSpaceSuffix }
    textField.text = text
    return false
}


func textFieldDidEndEditing(_ textField: UITextField) {
        textField.text = textField.text?.replacingOccurrences(of: Self.trailingSpaceSuffix, with: "")
          .trimmingCharacters(in: .whitespacesAndNewlines)
 }
Freezer answered 7/10, 2023 at 13:5 Comment(0)
B
0

My following solution also takes care of the problem with the cursor jumping to the end when typing a space in the middle or beginning of the string. Also pasting a string is now processed correctly too.

I also put in a check for email address fields and other checks, but the interesting part is the last part. It works perfectly for me, have yet to find a problem with it.

You can directly copy/paste this in your project. Don't forget to implement the didBeginEditing and didEndEditing to replace the spaces with non-breaking spaces and back!

- (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;
}
Bioclimatology answered 30/11, 2014 at 18:54 Comment(0)
I
0

I've used Jack Song's answer for Swift 2 for a while until I realized that the non-braking spaces make problems when rendered in HTML elsewhere, as well as line breaking gets messy in the UITextView itself. So, I've improved the solution to have the non-bracking characters cleaned right away.

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;
    }
}
Italian answered 12/9, 2016 at 13:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.