Повторно применить форматирование валюты к UITextField в событии изменения - PullRequest
9 голосов
/ 05 марта 2010

Я работаю с UITextField, который содержит значение локализованной валюты. Я видел много сообщений о том, как с этим работать, но мой вопрос: как мне повторно применить форматирование валюты к UITextField после каждого нажатия клавиши?

Я знаю, что могу настроить и использовать средство форматирования валюты с:

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

но я не знаю, как это подключить.

Например, если значение в поле читается как «$ 12 345», а пользователь нажимает клавишу «6», то значение должно измениться на «$ 123 456».

Какой обратный вызов является «правильным» для этого (следует ли мне использовать textField:shouldChangeCharactersInRange:replacementString: или пользовательское целевое действие) и как использовать NSNumberFormatter для анализа и повторного применения форматирования к свойству текста UITextField?

Любая помощь будет высоко ценится! Спасибо!

Ответы [ 6 ]

9 голосов
/ 27 мая 2010

Я работал над этой проблемой и думаю, что нашел хорошее, чистое решение. Я покажу вам, как соответствующим образом обновить текстовое поле при вводе пользователем, но вам придется самостоятельно определить локализацию, в любом случае эта часть должна быть достаточно простой.

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

Это код метода UITextFieldDelegate 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.
}

Редактировать 1: исправлена ​​утечка памяти.

Редактировать 2: обновлено для Умки - обработка 0 после десятичного разделителя. Код также обновлен для ARC.

4 голосов
/ 06 сентября 2012

Вот моя версия этого.

Настройте форматер где-нибудь:

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

Затем используйте его для анализа и форматирования 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 голосов
/ 15 июля 2012

Это была хорошая основа, но все еще не удовлетворяла потребности моего приложения. Вот что я приготовил, чтобы помочь ему. Я понимаю, что код на самом деле громоздок, и он здесь скорее для того, чтобы дать подсказку и, возможно, для того, чтобы получить более элегантное решение.

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

Форматтер чисел инициализируется следующим образом:

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

Он учитывает локали, поэтому должен нормально работать в разных локалях. Также поддерживаются следующие входные данные (примеры):

0,00 0,01

1,333,333.03

Пожалуйста, кто-нибудь улучшит это. Тема довольно интересная, элегантного решения пока не существует (в iOS нет функции setFormat ()).

2 голосов
/ 22 ноября 2013

Для простого ввода валюты в стиле банкомата, я использую следующий код, который работает довольно хорошо: , если , вы используете опцию UIKeyboardTypeNumberPad, которая разрешает только цифры и символы возврата.

Сначала настройте NSNumberFormatter так:

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

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

Затем используйте следующий код shouldChangeCharactersInRange:

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

Большая часть работы выполняется при установке значения newText; предлагаемое изменение используется, но все нецифровые символы, введенные NSNumberFormatter, удаляются.

Тогда первый if test проверяет, является ли текст пустым или все нули (помещаются туда пользователем или форматером). Если нет, следующий тест проверяет, что никакие входные данные, кроме обратных символов, не будут приняты, если число уже имеет максимальное количество цифр.

Последняя строка повторно отображает число с использованием средства форматирования, поэтому пользователь всегда видит что-то вроде $1,234.50. Обратите внимание, что я использую $0.00 для текста заполнителя с этим кодом, поэтому при установке значения nil отображается текст заполнителя.

1 голос
/ 31 октября 2013

Я написал для этого подкласс UITextField с открытым исходным кодом, доступный здесь:

https://github.com/TomSwift/TSCurrencyTextField

Он очень прост в использовании - просто поместите его на место любого UITextField. Вы все еще можете предоставить UITextFieldDelegate, если хотите, но это не обязательно.

0 голосов
/ 05 декабря 2013

Этот ответ основывается на коде Майкла Клосона, чтобы исправить некоторые ошибки региональной валюты, в некоторых местах его код всегда выдавал значение 0 при окончательном преобразовании обратно в строку textField. Я также нарушил задание newText, чтобы некоторым из нас, более новым программистам, было легче переваривать, иногда мне было утомительно вложение. Я отключил все параметры местоположения NSLocale и их значение MaximumFractionDigits, когда для них установлено значение NSNUmberFormatter. Я обнаружил, что было только 3 варианта. Большинство используют 2, некоторые другие не используют ни одного, а некоторые используют 3. Этот метод делегата текстового поля обрабатывает все 3 возможности, чтобы обеспечить надлежащий формат в зависимости от региона.

Вот мой метод делегата textField, в основном просто переписывающий код Майкла, но с расширенным назначением newText, добавленной поддержкой региональной десятичной дроби и использованием NSDecimalNumbers в качестве того, что я храню в своей базовой модели данных.

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