UITextField Format For Currency Revisited

By | December 2, 2011

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.

12 thoughts on “UITextField Format For Currency Revisited

  1. Darren

    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

    Reply
  2. Molly

    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.

    Reply
  3. jon

    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).

    Reply
  4. pierre

    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.

    Reply
  5. Darren

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

    Reply
  6. Darren

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

    Reply
  7. zs

    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.

    Reply
  8. Mueeid Khan

    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

    Reply

Leave a Reply to jon Cancel reply

Your email address will not be published. Required fields are marked *