A while ago I needed to allow users to enter a monetary value into a text input in an iOS app. There seem to be a few approaches to doing this, such as:
- Use a text field with a full keyboard and validate what the user entered, or only accept numeric input and a period or comma character
- Use a text field with a numeric keypad, but don't allow minor units (cents/pence) as there is no period or comma character available on this keypad
- Use spinners
To me, none of these options gives a good user experience; I find the best approach is what many ATMs do, which is to allow you to enter only the digits, but have a fixed decimal separator. So, to enter '123.45' you would type 1-2-3-4-5; to enter '18.00' you would type 1-8-0-0; and so on.
In this post, I am going to do this in iOS with a text input and a numeric keypad. The Objective-C code for this is available on github.
To start, I used interface builder to place a UITextField
control and a UIButton
control on a view, then created a UIViewController
and hooked up the text field to an instance variable in the view controller. I also declared the view controller as implementing the UITextFieldDelegate
protocol:
@interface IHViewController : UIViewController
I could then link up the view controller as the text field's delegate in interface builder. The textField:shouldChangeCharactersInRange:replacementString:
callback to the text field delegate gives us a useful place to manipulate the data in the text field as the user interacts with it (through typing on the keypad, deleting, cutting or pasting).
The approach I've taken is:
- Update the string in the textfield based on the user's input
- Strip out the decimal separator
- Convert the string to an integer value
- Divide the integer value by the appropriate amount depending on the number of decimal places needed (for example, by 100 for 2 decimal places)
- Use
NSString.stringWithFormat
to generate a formatted number
It's actually simpler than these steps suggest. Here's the code:
- (BOOL)textField:(UITextField*)textField shouldChangeCharactersInRange:(NSRange)range
replacementString:(NSString*)string {
// Update the string in the text input
NSMutableString* currentString = [NSMutableString stringWithString:textField.text];
[currentString replaceCharactersInRange:range withString:string];
// Strip out the decimal separator
[currentString replaceOccurrencesOfString:self.decimalSeparator withString:@""
options:NSLiteralSearch range:NSMakeRange(0, [currentString length])];
// Generate a new string for the text input
int currentValue = [currentString intValue];
NSString* format = [NSString stringWithFormat:@"%%.%df", self.maximumFractionDigits];
double minorUnitsPerMajor = pow(10, self.maximumFractionDigits);
NSString* newString = [NSString stringWithFormat:format, currentValue/minorUnitsPerMajor];
textField.text = newString;
return NO;
}
I decided to limit the length of the value too, by adding a check around the assignment to textField.text
:
#define MAX_LENGTH 8
if (newString.length <= MAX_LENGTH) {
textField.text = newString;
}
I initialise the maximumFractionDigits
and decimalSeparator
instance variables in the constructor using a NSNumberFormatter
, which can tell you what they should be for the specified locale or currency. If the user is entering a fixed precision number and not a currency, these could be hard-coded instead.
NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
[numberFormatter setCurrencyCode:@"GBP"];
self.maximumFractionDigits = numberFormatter.maximumFractionDigits;
self.decimalSeparator = numberFormatter.decimalSeparator;
[numberFormatter release];
And that's it. Clone the code from github to see a full, working implementation of this.
Update:
As pointed out in a comment by Trevor, the cursor does not behave as expected when it's not at the end of the text in the control (such as when the user moves the cursor to the middle of the text and types). I've updated the code on github to keep the cursor in the correct position. In the textField:shouldChangeCharactersInRange:replacementString:
method, before updating the text in the control, the current cursor position and the length of the current text are stored:
// get current cursor position
UITextRange* selectedRange = [textField selectedTextRange];
UITextPosition* start = textField.beginningOfDocument;
NSInteger cursorOffset = [textField offsetFromPosition:start toPosition:selectedRange.start];
// ..
NSUInteger currentLength = currentString.length;
After updating the text in the control, the cursor position is restored:
// if the cursor was not at the end of the string being entered, restore cursor position
if (cursorOffset != currentLength) {
int lengthDelta = newString.length - currentLength;
int newCursorOffset = MAX(0, MIN(newString.length, cursorOffset + lengthDelta));
UITextPosition* newPosition = [textField positionFromPosition:textField.beginningOfDocument offset:newCursorOffset];
UITextRange* newRange = [textField textRangeFromPosition:newPosition toPosition:newPosition];
[textField setSelectedTextRange:newRange];
}
Note: The methods I've used are from the UITextInput
protocol, which UITextField
implements in iOS5 and up. The cursor positioning code therefore only works on iOS5 and up.
Check out the updated code in the github repo.
There is still one bit of behaviour which could be better: when the cursor is to the right of the decimal point and the user hits backspace, nothing happens. Ideally, the digit before the decimal point should be deleted. A future update will fix this.
Update 2:
As pointed out by Alexander Zubkov, the code wasn't working properly for locales with a decimal separator that is not a period. I was not taking the decimal separator into account everywhere. I've made some changes to the code to fix the issue.

Posted Fri 13 Jul 2012 by Michael Patricios , updated Thu 28 Nov 2013
Tags: Development, iOS, Objective-C
Comments
Great solution, thanks. And good call on the internationalization.
This works great, thanks!
Unfortunately this does not work as the user expects if he positions the cursor in the middle of the text and then adds/deletes a character. In particular, the cursor jumps to the end of the field.
Trevor - indeed, the cursor jumping to the end when you manually place it in the middle of the text and type is one edge case which doesn't work quite right. I'll look at updating the code.
Trevor - I've updated the code to correct the cursor jumping to the end of the field.
Hi,
Would you please be kind enough to have a look at my issue here, I believe you may be able to resolve what I am after.
http://stackoverflow.com/questions/18264649/ios-typing-into-an-empty-uitextview-with-formatting-with-left-and-right-padding
Basically I trying to type into an Empty UITextView with formatting With Left and Right Padding? I have worked out the formatting but I can't workout how I can use shouldChangeTextInRange to maintain the formatting and the cursor position.
Thanking you in advance,
Will
It won't give the desired result for users from Germany or Russia.
I assume it's because of NSNumberFormatter that gives different decimal separator.
When you push any digit it doesn't add it. Instead if you have any preloaded number (12.05) it will clean it up to 0.00.
Alexander, you are right. Thanks for spotting that it didn't work when the decimal separator is not a period. I should have tested it for different locales. I have now fixed it so it does work (see commit in github for changes).
Nice, I would like ti know how will you approach the case if you want to update the String/Number that is unputed by the user for example 1000 to 1,000 adding the coma while the user is entering the number?
swift: