Since my first post on formatting a UITextField with proper currency symbols and separators I think I have learned a lot more about objective-c and more importantly the UI components provided by Apple. In this article I will revisit how to format a UITextField with the correct currency symbol and grouping separators.
The original goal was to create a text field that would update in real time both with the currency symbol and grouping separators as numbers were typed in. The first working solution can be found here, but it had some short comings particularly in code complexity. The main problem was that I was trying to keep the string formatting correct so that I could decode the string to an NSNumber using a currency formatter and then encode the string again to an NSString. This was needless complicating the code when dealing with inputting characters. The original code:
//MARK: -
//MARK: UITextFieldDelegate Implementation
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range
replacementString:(NSString *)string{
BOOL result = NO; //default to reject
if([string length] == 0){ //backspace
result = YES;
}
else{
if([string stringByTrimmingCharactersInSet:nonNumberSet].length > 0){
result = YES;
}
}
//here we deal with the UITextField on our own
if(result){
//grab a mutable copy of what's currently in the UITextField
NSMutableString* mstring = [[textField text] mutableCopy];
if([mstring length] == 0){
//special case...nothing in the field yet, so set a currency symbol first
[mstring appendString:[[NSLocale currentLocale] objectForKey:NSLocaleCurrencySymbol]];
//now append the replacement string
[mstring appendString:string];
}
else{
//adding a char or deleting?
if([string length] > 0){
[mstring insertString:string atIndex:range.location];
}
else {
//delete case - the length of replacement string is zero for a delete
[mstring deleteCharactersInRange:range];
}
}
//to get the grouping separators properly placed
//first convert the string into a number. The function
//will ignore any grouping symbols already present -- NOT in iOS4!
//fix added below - remove locale specific currency separators first
NSString* localeSeparator = [[NSLocale currentLocale]
objectForKey:NSLocaleGroupingSeparator];
NSNumber* number = [currencyFormatter numberFromString:[mstring
stringByReplacingOccurrencesOfString:localeSeparator
withString:@""]];
[mstring release];
//now format the number back to the proper currency string
//and get the grouping separators added in and put it in the UITextField
[textField setText:[currencyFormatter stringFromNumber:number]];
}
//always return no since we are manually changing the text field
return NO;
}
Instead it was much easier to just strip out the currency symbol and group separators and then use a basic number formatter to turn the string into a NSNumber. I was able to remove an entire section of code:
...
//here we deal with the UITextField on our own
if(result){
//grab a mutable copy of what's currently in the UITextField
NSMutableString* mstring = [[textField text] mutableCopy];
if([mstring length] == 0){
//special case...nothing in the field yet, so set a currency symbol first
[mstring appendString:[[NSLocale currentLocale] objectForKey:NSLocaleCurrencySymbol]];
//now append the replacement string
[mstring appendString:string];
}
...
Finally, take the newly formed NSNumber and run it through the currency formatter to create the desired output. The entire new method is below:
//MARK: -
//MARK: UITextFieldDelegate Implementation
-(BOOL)textField:(UITextField *)textField
shouldChangeCharactersInRange:(NSRange)range
replacementString:(NSString *)string{
BOOL result = NO; //default to reject
if([string length] == 0){ //backspace
result = YES;
}
else{
if([string stringByTrimmingCharactersInSet:nonNumberSet].length > 0){
result = YES;
}
}
//here we deal with the UITextField on our own
if(result){
//grab a mutable copy of what's currently in the UITextField
NSMutableString* mstring = [[textField text] mutableCopy];
//adding a char or deleting?
if([string length] > 0){
[mstring insertString:string atIndex:range.location];
}
else {
//delete case - the length of replacement string is zero for a delete
[mstring deleteCharactersInRange:range];
}
//remove any possible symbols so the formatter will work
NSString* clean_string = [[mstring stringByReplacingOccurrencesOfString:localGroupingSeparator
withString:@""]
stringByReplacingOccurrencesOfString:localCurrencySymbol
withString:@""];
//clean up mstring since it's no longer needed
[mstring release];
NSNumber* number = [basicFormatter numberFromString: clean_string];
//now format the number back to the proper currency string
//and get the grouping separators added in and put it in the UITextField
[textField setText:[currencyFormatter stringFromNumber:number]];
}
//always return no since we are manually changing the text field
return NO;
}
First thing to notice is the cleanup of the code between lines 19 and 21. There is no more special case, and thus reduced complexity. Line 32 is what allowed the removal of the special case. Instead of trying to work with symbols and groupings they are now ignored completely and removed before being added back in. The variable clean_string will end up a string with only numbers and will be easily parsed into an NSNumber by a basic NSNumberFormatter.
There are a few class instance variables that need to be set. The init and associated dealloc is below even though they are not necessarily related to the optimizations above.
//MARK: -
//MARK: Object Management
-(id)init{
if ((self = [super init])){
NSLocale* locale = [NSLocale currentLocale];
localCurrencySymbol = [locale objectForKey:NSLocaleCurrencySymbol];
localGroupingSeparator = [locale objectForKey:NSLocaleGroupingSeparator];
currencyFormatter = [[Formatters currencyFormatterWithNoFraction] retain];
basicFormatter = [[Formatters basicFormatter] retain];
//set up the reject character set
NSMutableCharacterSet *numberSet = [[NSCharacterSet decimalDigitCharacterSet] mutableCopy];
[numberSet formUnionWithCharacterSet:[NSCharacterSet whitespaceCharacterSet]];
nonNumberSet = [[numberSet invertedSet] retain];
[numberSet release];
}
return self;
}
-(void)dealloc {
[nonNumberSet release];
[currencyFormatter release];
[localCurrencySymbol release];
[localGroupingSeparator release];
[basicFormatter release];
[super dealloc];
}
Formatters is a custom class that contains various class methods to return ready to use formatters. In the next article I will show how it is setup. In the meantime two new NSNumberFormatters could have just been declared inline. See the Apple reference documentation for NSNumberFormatter.
The above improvement cut out some code that was not needed, but most of all made the logic easier to follow. Instead of having to worry about special cases with currency symbols when inserting new characters, they can now just be ignored. The formatters do all of the heavy lifting as they should.
Michael, Thank you so much for this useful code.
Do you know how it should be modified to work with 2 decimal places (pennies)?
Thanks
Been tinkering for hours, got it sorted with decimals now 🙂
Hey Darren,
I'm glad this was helpful! When I get some time I want to turn this into a user control and put it up on github.
Michael, thanks for this article. When you get a chance can you upload your 'Formatters' class? That completes this article and we can use this for real.
Thanks.
Molly.
Molly, it's a pretty simple class and just encapsulates the creation of certain formatters. I went ahead put the class up on github here:
https://github.com/matwood/NSNumberFormatterHelper
I'm glad you found this article useful.
Thanks for this code, it was very helpful to get me started.
A few notes on localization:
– regarding decimals, it might be useful to leave the number of decimal places up to the user's locale with the technique outlined in the answer to this question: http://stackoverflow.com/questions/276382/what-is-the-best-way-to-enter-numeric-values-with-decimal-points
– also, when you're stripping out the currency symbols and group separators, I found it necessary to also strip out blank spaces as well, because with some currencies (like if you set the region to France), the formatter inserts a blank space and puts the currency symbol after the amount (instead of in front like for USA).
Thanks Micheal, this code is very helpful.
Hi Daren, please can you share your outcome with regard to the decimal place issue you experienced. I am from South Africa had would like to do the same in my App with our currency Rand / cents. I have spend a lot of time trying to resolve this issue. Much appreciated if you can share the code.
I can't remember now what had to be done. Here's my shouldChangeCharactersInRange method that might help you: (Had to split it into 2 posts)
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
if (textField == self.description) return YES;
if (textField == self.oddsField || textField == self.oddsFrom || textField == self.oddsTo) {
NSMutableString *mstring = [[textField text] mutableCopy];
// Adding a char or deleting
if ([string length] > 0) {
[mstring insertString:string atIndex:range.location];
}
else {
// Delete case – the length of replacement string is zero for a delete
[mstring deleteCharactersInRange:range];
}
[textField setText:mstring];
NSLog(@"textField = %@",mstring);
[self calculateStakeNeeded];
return NO;
}
BOOL result = NO; // Default to reject
BOOL containsDecimal = NO;
if ([string length] == 0) { // Backspace
result = YES;
} // Backspace Result=Yes
else {
if ([string stringByTrimmingCharactersInSet:nonNumberSet].length > 0) {
result = YES;
}
}
// Here we deal with the UITextField on our own
if (result) {
// Grab a mutable copy of what's currently in the UITextField
NSMutableString *mstring = [[textField text] mutableCopy];
NSRange range2 = [mstring rangeOfString:localDecimalSeperator];
if (range2.location != NSNotFound) {
NSLog(@"Found decimal point");
containsDecimal = YES;
} // Check for decimal point and set containsDecimal
// Adding a char or deleting
if ([string length] > 0) {
[mstring insertString:string atIndex:range.location];
} //Adding
else {
// Delete case – the length of replacement string is zero for a delete
[mstring deleteCharactersInRange:range];
if ([mstring hasSuffix:localDecimalSeperator]) {
NSLog(@"Last digit is .");
[textField setText:mstring];
return NO;
} else if ([[mstring stringByReplacingOccurrencesOfString:[localDecimalSeperator stringByAppendingString:@"0"] withString:localDecimalSeperator] hasSuffix:localDecimalSeperator]) {
[textField setText:mstring];
return NO;
}
} // Deleting
if ([string isEqualToString:localDecimalSeperator]) {
NSLog(@"Pressed Decimal");
if (containsDecimal) {
return NO;
} else {
[textField setText:mstring];
}
} else if ([string isEqualToString:@"0"] && containsDecimal) {
NSLog(@"Pressed 0 and contains decimal");
[textField setText:mstring];
} else {
// Remove any possible symbols so the formatter will work
NSString *clean_string = [[mstring stringByReplacingOccurrencesOfString:localGroupingSeperator withString:@""] stringByReplacingOccurrencesOfString:localCurrencySymbol withString:@""];
NSNumber *number = [[Formatters currencyFormatterBasic] numberFromString:clean_string];
// Now format the number back to the proper currency string
// and get the grouping seperators added in and put it in the UITextField
if ([[Formatters currencyFormatterDecimal] stringFromNumber:number] == nil) {
[textField setText:@""];
} else {
[textField setText:[localCurrencySymbol stringByAppendingString:[[Formatters currencyFormatterDecimal] stringFromNumber:number]]];
}
}
}
// Always return no since we are manually changing the text field
if (textField == self.amountWantedTextfield) {
[self calculateStakeNeeded];
}
if (textField == self.amountField) {
[self shouldEnableSaveButton];
}
return NO;
}
Darren, that's a great solution. Works great … almost! It seems to be problematic for currencies which have the currency symbol on the right of the number instead of the left. For e.g. Euros. Trying to figure out how to work around that now.
A currency union (also known as monetary union) is where two or more states share the same currency, though without there necessarily having any further. gold buyers nj
If you've sold a currency pair, it's called going short or getting short also it means you're searching for the pair's price to advance lower to help you to buy it back for a profit. In the event you sell at various prices, you're contributing to shorts and getting shorter.guarantor loans