Re-Apply currency formatting to a UITextField on a change event

可紊 提交于 2019-12-17 06:13:19

问题


I'm working with a UITextField that holds a localized currency value. I've seen lots of posts on how to work with this, but my question is: how do I re-apply currency formatting to the UITextField after every key press?

I know that I can set up and use a currency formatter with:

NSNumberFormatter *currencyFormatter = [[NSNumberFormatter alloc] init];
[currencyFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
...
[currencyFormatter stringFromNumber:...];

but I don't know how to hook it up.

For instance, if the value in the field reads "$12,345" and the user taps the "6" key, then the value should change to "$123,456".

Which callback is the "correct" one to do this in (should I use textField:shouldChangeCharactersInRange:replacementString: or a custom target-action) and how do I use the NSNumberFormatter to parse and re-apply formatting to the UITextField's text property?

Any help would be much appreciated! Thanks!


回答1:


I've been working on this issue and I think I figured out a nice, clean solution. I'll show you how to appropriately update the textfield on user input, but you'll have to figure out the localization yourself, that part should be easy enough anyway.

- (void)viewDidLoad
{
    [super viewDidLoad];


    // setup text field ...

#define PADDING 10.0f

    const CGRect bounds = self.view.bounds;
    CGFloat width       = bounds.size.width - (PADDING * 2);
    CGFloat height      = 30.0f;
    CGRect frame        = CGRectMake(PADDING, PADDING, width, height);

    self.textField                      = [[UITextField alloc] initWithFrame:frame];
    _textField.backgroundColor          = [UIColor whiteColor];
    _textField.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
    _textField.autocapitalizationType   = UITextAutocapitalizationTypeNone;
    _textField.autocorrectionType       = UITextAutocorrectionTypeNo;
    _textField.text                     = @"0";
    _textField.delegate                 = self;
    [self.view addSubview:_textField];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];


    // force update for text field, so the initial '0' will be formatted as currency ...

    [self textField:_textField shouldChangeCharactersInRange:NSMakeRange(0, 0) replacementString:@"0"];
}

- (void)viewDidUnload
{
    self.textField = nil;

    [super viewDidUnload];
}

This is the code in the UITextFieldDelegate method textField:shouldChangeCharactersInRange:replacementString:

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    NSString *text             = _textField.text;
    NSString *decimalSeperator = @".";
    NSCharacterSet *charSet    = nil;
    NSString *numberChars      = @"0123456789";


    // the number formatter will only be instantiated once ...

    static NSNumberFormatter *numberFormatter;
    if (!numberFormatter)
    {
        numberFormatter = [[NSNumberFormatter alloc] init];
        numberFormatter.numberStyle           = NSNumberFormatterCurrencyStyle;
        numberFormatter.maximumFractionDigits = 10;
        numberFormatter.minimumFractionDigits = 0;
        numberFormatter.decimalSeparator      = decimalSeperator;
        numberFormatter.usesGroupingSeparator = NO;
    }


    // create a character set of valid chars (numbers and optionally a decimal sign) ...

    NSRange decimalRange = [text rangeOfString:decimalSeperator];
    BOOL isDecimalNumber = (decimalRange.location != NSNotFound);
    if (isDecimalNumber)
    {
        charSet = [NSCharacterSet characterSetWithCharactersInString:numberChars];        
    }
    else
    {
        numberChars = [numberChars stringByAppendingString:decimalSeperator];
        charSet = [NSCharacterSet characterSetWithCharactersInString:numberChars];
    }


    // remove amy characters from the string that are not a number or decimal sign ...

    NSCharacterSet *invertedCharSet = [charSet invertedSet];
    NSString *trimmedString = [string stringByTrimmingCharactersInSet:invertedCharSet];
    text = [text stringByReplacingCharactersInRange:range withString:trimmedString];


    // whenever a decimalSeperator is entered, we'll just update the textField.
    // whenever other chars are entered, we'll calculate the new number and update the textField accordingly.

    if ([string isEqualToString:decimalSeperator] == YES) 
    {
        textField.text = text;
    }
    else 
    {
        NSNumber *number = [numberFormatter numberFromString:text];
        if (number == nil) 
        {
            number = [NSNumber numberWithInt:0];   
        }
        textField.text = isDecimalNumber ? text : [numberFormatter stringFromNumber:number];
    }

    return NO; // we return NO because we have manually edited the textField contents.
}

Edit 1: fixed memory leak.

Edit 2: updated for Umka - handling of 0 after decimal separator. Code is updated for ARC as well.




回答2:


This was good basis but still did not satisfy my app needs. Here is what I cooked to help it. I understand that code is actually cumbersome and it is here rather to give a clue and may be to get more elegant solution.

- (BOOL)textField:(UITextField *)aTextField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{   
        if (aTextField == myAmountTextField) {
                NSString *text = aTextField.text;
                NSString *decimalSeperator = [[NSLocale currentLocale] objectForKey:NSLocaleDecimalSeparator];
                NSString *groupSeperator = [[NSLocale currentLocale] objectForKey:NSLocaleGroupingSeparator];

                NSCharacterSet *characterSet = nil;
                NSString *numberChars = @"0123456789";
                if ([text rangeOfString:decimalSeperator].location != NSNotFound)
                        characterSet = [NSCharacterSet characterSetWithCharactersInString:numberChars];
                else
                        characterSet = [NSCharacterSet characterSetWithCharactersInString:[numberChars stringByAppendingString:decimalSeperator]];

                NSCharacterSet *invertedCharSet = [characterSet invertedSet];   
                NSString *trimmedString = [string stringByTrimmingCharactersInSet:invertedCharSet];
                text = [text stringByReplacingCharactersInRange:range withString:trimmedString];

                if ([string isEqualToString:decimalSeperator] == YES ||
                    [text rangeOfString:decimalSeperator].location == text.length - 1) {
                        [aTextField setText:text];
                } else {
                        /* Remove group separator taken from locale */
                        text = [text stringByReplacingOccurrencesOfString:groupSeperator withString:@""];


                        /* Due to some reason, even if group separator is ".", number
                        formatter puts spaces instead. Lets handle this. This all should
                        be done before converting to NSNUmber as otherwise we will have
                        nil. */
                        text = [text stringByReplacingOccurrencesOfString:@" " withString:@""];
                        NSNumber *number = [numberFormatter numberFromString:text];
                        if (number == nil) {
                                [textField setText:@""];
                        } else {
                                /* Here is what I call "evil miracles" is going on :)
                                Totally not elegant but I did not find another way. This
                                is all needed to support inputs like "0.01" and "1.00" */
                                NSString *tail = [NSString stringWithFormat:@"%@00", decimalSeperator];
                                if ([text rangeOfString:tail].location != NSNotFound) {
                                        [numberFormatter setPositiveFormat:@"#,###,##0.00"];
                                } else {
                                        tail = [NSString stringWithFormat:@"%@0", decimalSeperator];
                                        if ([text rangeOfString:tail].location != NSNotFound) {
                                                [numberFormatter setPositiveFormat:@"#,###,##0.0#"];
                                        } else {
                                                [numberFormatter setPositiveFormat:@"#,###,##0.##"];
                                        }
                                }
                                text = [numberFormatter stringFromNumber:number];
                                [textField setText:text];
                        }
                }

                return NO;
        }
        return YES;
}

Number formatter is initialized in a way like this:

numberFormatter = [[NSNumberFormatter alloc] init]; 
[numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
numberFormatter.roundingMode = kCFNumberFormatterRoundFloor;

It is locale aware, so should work fine in different locale setups. It also supports the following inputs (examples):

0.00 0.01

1,333,333.03

Please somebody improve this. Topic is kind of interesting, no elegant solution exists so far (iOS has not setFormat() thing).




回答3:


Here's my version of this.

Setup a formatter some place:

    // Custom initialization
    formatter = [NSNumberFormatter new];
    [formatter setNumberStyle: NSNumberFormatterCurrencyStyle];
    [formatter setLenient:YES];
    [formatter setGeneratesDecimalNumbers:YES];

Then use it to parse and format the UITextField:

-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    NSString *replaced = [textField.text stringByReplacingCharactersInRange:range withString:string];
    NSDecimalNumber *amount = (NSDecimalNumber*) [formatter numberFromString:replaced];
    if (amount == nil) {
        // Something screwed up the parsing. Probably an alpha character.
        return NO;
    }
    // If the field is empty (the inital case) the number should be shifted to
    // start in the right most decimal place.
    short powerOf10 = 0;
    if ([textField.text isEqualToString:@""]) {
        powerOf10 = -formatter.maximumFractionDigits;
    }
    // If the edit point is to the right of the decimal point we need to do
    // some shifting.
    else if (range.location + formatter.maximumFractionDigits >= textField.text.length) {
        // If there's a range of text selected, it'll delete part of the number
        // so shift it back to the right.
        if (range.length) {
            powerOf10 = -range.length;
        }
        // Otherwise they're adding this many characters so shift left.
        else {
            powerOf10 = [string length];
        }
    }
    amount = [amount decimalNumberByMultiplyingByPowerOf10:powerOf10];

    // Replace the value and then cancel this change.
    textField.text = [formatter stringFromNumber:amount];
    return NO;
}



回答4:


For simple ATM style currency entry, I use the following code, which works quite well, if you use the UIKeyboardTypeNumberPad option, which allows only digits and backspaces.

First set up an NSNumberFormatter like this:

NSNumberFormatter* currencyFormatter = [[NSNumberFormatter alloc] init];

[currencyFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
[currencyFormatter setMaximumSignificantDigits:9]; // max number of digits including ones after the decimal here

Then, use the following shouldChangeCharactersInRange code:

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    NSString* newText = [[[[textField.text stringByReplacingCharactersInRange:range withString:string] stringByReplacingOccurrencesOfString:currencyFormatter.currencySymbol withString:[NSString string]] stringByReplacingOccurrencesOfString:currencyFormatter.groupingSeparator withString:[NSString string]] stringByReplacingOccurrencesOfString:currencyFormatter.decimalSeparator withString:[NSString string]];
    NSInteger newValue = [newText integerValue];

    if ([newText length] == 0 || newValue == 0)
        textField.text = nil;
    else if ([newText length] > currencyFormatter.maximumSignificantDigits)
        textField.text = textField.text;
    else
        textField.text = [currencyFormatter stringFromNumber:[NSNumber numberWithDouble:newValue / 100.0]];

    return NO;
}

Most of the work is done in setting the newText value; the proposed change is used but all non-digit characters put in by NSNumberFormatter are taken out.

Then the first if test checks to see if the text is empty or all zeros (put there by the user or the formatter). If not, the next test makes sure that no more input except backspaces will be accepted if the number is already at the maximum number of digits.

The final else re-displays the number using the formatter so the user always sees something like $1,234.50. Note that I use a $0.00 for the placeholder text with this code, so setting the text to nil shows the placeholder text.




回答5:


I wrote an open source UITextField subclass to handle this, available here:

https://github.com/TomSwift/TSCurrencyTextField

It's very easy to use - just drop it in place of any UITextField. You can still provide a UITextFieldDelegate if you like, but this is not required.




回答6:


This answer builds a little on Michael Klosson's code to fix some regional currency bugs, some locations caused his code to always spit out a 0 value in the final conversion back to textField string. I've also broke down the newText assignment to make it easier for some of us newer coders to digest, the nesting is tiresome for me sometimes. I logged out all NSLocale location options and their maximumFractionDigits value when an NSNUmberFormatter is set to them. I've found there were only 3 possibilities. Most use 2, some others use none, and a few use 3. This textfield delegate method handles all 3 possibilities, to ensure proper format based on region.

Here is my textField delegate method, mainly just a rewrite of Michael's code, but with the newText assignment expanded out, regional decimal support added, and using NSDecimalNumbers as thats what I'm storing in my core data model anyway.

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
        // get the format symbols
        NSNumberFormatter *currencyFormatter = self.currencyFormatter;
        NSString *currencySymbol = currencyFormatter.currencySymbol;
        NSString *groupSeperator = currencyFormatter.groupingSeparator;
        NSString *decimalSeperator = currencyFormatter.decimalSeparator;

        // strip all the format symbols to leave an integer representation
        NSString* newText = [textField.text stringByReplacingCharactersInRange:range withString:string];
        newText = [newText stringByReplacingOccurrencesOfString:currencySymbol withString:@""];
        newText = [newText stringByReplacingOccurrencesOfString:groupSeperator withString:@""];
        newText = [newText stringByReplacingOccurrencesOfString:decimalSeperator withString:@""];

        NSDecimalNumber *newValue = [NSDecimalNumber decimalNumberWithString:newText];

        if ([newText length] == 0 || newValue == 0)
            textField.text = nil;
        else if ([newText length] > currencyFormatter.maximumSignificantDigits) {
    //      NSLog(@"We've maxed out the digits");
            textField.text = textField.text;
        } else {
            // update to new value
            NSDecimalNumber *divisor;
            // we'll need a number to divide by if local currency uses fractions
            switch (currencyFormatter.maximumFractionDigits) {
                // the max fraction digits tells us the number of decimal places
                // divide accordingly based on the 3 available options
                case 0:
    //              NSLog(@"No Decimals");
                    divisor = [NSDecimalNumber decimalNumberWithString:@"1.0"];
                    break;
                case 2:
    //              NSLog(@"2 Decimals");
                    divisor = [NSDecimalNumber decimalNumberWithString:@"100.0"];
                    break;
                case 3:
    //              NSLog(@"3 Decimals");
                    divisor = [NSDecimalNumber decimalNumberWithString:@"1000.0"];
                    break;
                default:
                    break;
            }
            NSDecimalNumber *newAmount = [newValue decimalNumberByDividingBy:divisor];
            textField.text = [currencyFormatter stringFromNumber:newAmount];
        }
        return NO;
    }


来源:https://stackoverflow.com/questions/2388448/re-apply-currency-formatting-to-a-uitextfield-on-a-change-event

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!