Friday, December 2, 2011

UITextField Format For Currency Revisited

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:


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:


Finally, take the newly formed NSNumber and run it through the currency formatter to create the desired output.  The entire new method is below:


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.

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.

11 comments:

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

    ReplyDelete
  2. Been tinkering for hours, got it sorted with decimals now :-)

    ReplyDelete
  3. 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.

    ReplyDelete
  4. 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.

    ReplyDelete
  5. 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.

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

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

    ReplyDelete
  8. 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;
    }
    }

    ReplyDelete
  9. // 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;
    }

    ReplyDelete
  10. 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.

    ReplyDelete
  11. 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

    ReplyDelete