In my iPad app, I noticed different behavior between iOS 6 and iOS 7 with UITextFields.
I create the UITextField as follows:
UIButton *theButton = (U
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.
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.
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:
Somewhere add an action for the UIControlEventEditingChanged
event on your textfield:
[myTextField addTarget:self action:@selector(replaceNormalSpacesWithNonBreakingSpaces)
forControlEvents:UIControlEventEditingChanged];
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.
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
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:@" "];
}
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
}
}
}
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;
}
}