14

I want to have commas dynamically added to my numeric UITextField entry while the user is typing.

For example: 123,456 and 12,345,678, but not like this 123,45 or 123,4567.

How does one automatically append the commas while a user is typing a number in Objective-C?

Edit: I'd also like to be able to allow the user to input decimals.

Lyndsey Scott
  • 37,080
  • 10
  • 92
  • 128
Windy_Cheng
  • 153
  • 1
  • 1
  • 4

9 Answers9

23

Instead of inserting the commas on your own in shouldChangeCharactersInRange:, you can use an NSNumberFormatterDecimalStyle to handle the comma formatting for you. Even though it's called "decimal" style, it also inserts commas to appropriately group numbers into their thousands digits.

Note: To simplify matters, I'll assume you only want the text field to accept numeric entries and I'll also add logic to limit the user's input to numbers.

Edit: I've updated the code to handle decimals also as per the OP's request.

To utilize NSNumberFormatterDecimalStyle's formatting upon every character entry, try adding this to your shouldChangeCharactersInRange: delegate method:

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {

    if (([string isEqualToString:@"0"] || [string isEqualToString:@""]) && [textField.text rangeOfString:@"."].location < range.location) {
        return YES;
    }

    // First check whether the replacement string's numeric...
    NSCharacterSet *cs = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789"] invertedSet];
    NSString *filtered = [[string componentsSeparatedByCharactersInSet:cs] componentsJoinedByString:@""];
    bool isNumeric = [string isEqualToString:filtered];

    // Then if the replacement string's numeric, or if it's 
    // a backspace, or if it's a decimal point and the text
    // field doesn't already contain a decimal point,
    // reformat the new complete number using
    // NSNumberFormatterDecimalStyle
    if (isNumeric ||
        [string isEqualToString:@""] ||
        ([string isEqualToString:@"."] &&
         [textField.text rangeOfString:@"."].location == NSNotFound)) {

        // Create the decimal style formatter
        NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
        [formatter setNumberStyle:NSNumberFormatterDecimalStyle];
        [formatter setMaximumFractionDigits:10];

        // Combine the new text with the old; then remove any
        // commas from the textField before formatting
        NSString *combinedText = [textField.text stringByReplacingCharactersInRange:range withString:string];
        NSString *numberWithoutCommas = [combinedText stringByReplacingOccurrencesOfString:@"," withString:@""];
        NSNumber *number = [formatter numberFromString:numberWithoutCommas];

        NSString *formattedString = [formatter stringFromNumber:number];

        // If the last entry was a decimal or a zero after a decimal,
        // re-add it here because the formatter will naturally remove
        // it.
        if ([string isEqualToString:@"."] &&
            range.location == textField.text.length) {
            formattedString = [formattedString stringByAppendingString:@"."];
        }

        textField.text = formattedString;

    }

    // Return no, because either the replacement string is not 
    // valid or it is and the textfield has already been updated
    // accordingly
    return NO;
}
Lyndsey Scott
  • 37,080
  • 10
  • 92
  • 128
  • Yes,it works.But now can’t intput decimals, how to solve it ? For example, 12, 345.7890 – Windy_Cheng Dec 05 '14 at 06:07
  • @Windy_Cheng Oh, I didn't realize you also need decimals... You would need to edit this code to also allow for a single decimal point entry. I'm about to go to sleep now, but if you can't figure it out, I can help again in the morning. – Lyndsey Scott Dec 05 '14 at 06:14
  • @Windy_Cheng Actually, I just updated it to handle decimals too. – Lyndsey Scott Dec 05 '14 at 06:32
  • Sorry, It didn't work!Still can't input decimals!By the way,in the code [numberFormatter setMinimumFractionDigits:0]; [numberFormatter setMaximumFractionDigits:100]; What does doesnumberFormatter mean? Do you mean *formatter? – Windy_Cheng Dec 05 '14 at 08:11
  • @Windy_Cheng Yeah, I meant formatter. And I'm looking into why you still can't input decimals... One sec... – Lyndsey Scott Dec 05 '14 at 13:33
  • @Windy_Cheng Gotcha. It's not working because the number formatter will take out the decimal if there's nothing after it. I'll make an update. – Lyndsey Scott Dec 05 '14 at 13:49
  • This doesn't work for decimals followed by a zero... For instance: 425.04 – Gazzini Mar 16 '15 at 03:23
  • Hi could you please provide me the swift version of above code – Sunita Mar 25 '16 at 04:15
  • Thanks, @LyndseyScott. I did notice that if you place the cursor in the middle of the string to edit it, the formatting will automatically cause the cursor to move to the end rather than keep it where it is. A combination of your answer and [this one](http://stackoverflow.com/a/26284499/3711928) gives the results someone would probably expect. – j.f. Jul 25 '16 at 20:36
  • @Lyndsey Scott - code perfectly works on iOS 9.x, but in iOS 8.x it require additional code of one line to add support for decimal number. just add the following code after setNumberStyle, [formatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US"]]; // for iOS 8.. Now in iOS 8.x also works as per requirement, +1 for your help. – g212gs Oct 20 '16 at 07:15
14

Here is a version in Swift 4. I used it for integers, I didn't check with decimal numbers.

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

     // Uses the number format corresponding to your Locale
     let formatter = NumberFormatter()
     formatter.numberStyle = .decimal
     formatter.locale = Locale.current
     formatter.maximumFractionDigits = 0


    // Uses the grouping separator corresponding to your Locale
    // e.g. "," in the US, a space in France, and so on
    if let groupingSeparator = formatter.groupingSeparator {

        if string == groupingSeparator {
            return true
        }


        if let textWithoutGroupingSeparator = textField.text?.replacingOccurrences(of: groupingSeparator, with: "") {
            var totalTextWithoutGroupingSeparators = textWithoutGroupingSeparator + string
            if string.isEmpty { // pressed Backspace key
                totalTextWithoutGroupingSeparators.removeLast()
            }
            if let numberWithoutGroupingSeparator = formatter.number(from: totalTextWithoutGroupingSeparators),
                let formattedText = formatter.string(from: numberWithoutGroupingSeparator) {

                textField.text = formattedText
                return false
            }
        }
    }
    return true
}

The big advantage of this method is that it uses the grouping separator defined in your current locale (region), because not everybody uses the comma as a grouping separator.

Works with 0, backspace, but, again, I didn't test it with decimals. You are free to enhance this code if you worked it out with decimals.

Examples:

  • Enter : "2" -> "2"
  • Enter : "3" -> "23"
  • Enter : "6" -> "236"
  • Enter : "7" -> "2,367"
  • Enter : "0" 3 times -> "2,367,000"
  • Backspace -> "236,700"

Starting 0 works too:

  • Enter : "0" -> "0"
  • Enter : "2" -> "2"
Swan
  • 305
  • 2
  • 6
Frederic Adda
  • 5,905
  • 4
  • 56
  • 71
  • This works good, but I encountered a messy bug when the user hold down the `Delete` key. You have to edit your code with this `if totalTextWithoutGroupingSeparators.characters.count > 0 && string == "" { // pressed Backspace key totalTextWithoutGroupingSeparators.characters.removeLast() }` – Paul Razvan Berg Mar 31 '17 at 08:47
  • This code works fine, but i need to integrate with decimal point like 236,700.00 – Balaji Malliswamy Jul 06 '17 at 07:43
  • You have to make the disctinction between the *groupingSeparator* (the comma between thousands in your example), and the *decimalSeparator* (the dot before the decimals) – Frederic Adda Jul 06 '17 at 07:45
  • I like it but I put this code at the beginning to make sure only numbers were entered: guard CharacterSet(charactersIn: "0123456789").isSuperset(of: CharacterSet(charactersIn: string)) else { return false } – Narwhal Jan 01 '18 at 06:15
  • You don’t need that. Making sure that number(from: String) is not nil is sufficient. Also you don’t want to get rid of backspace. – Frederic Adda Jan 01 '18 at 06:38
  • This will continuously creates formatter object, make it global – karthikeyan Jun 20 '19 at 09:14
11

For Swift 4.0 Version of Lyndsey Scott's answer:

   func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        if ((string == "0" || string == "") && (textField.text! as NSString).range(of: ".").location < range.location) {
            return true
        }

        // First check whether the replacement string's numeric...
        let cs = NSCharacterSet(charactersIn: "0123456789.").inverted
        let filtered = string.components(separatedBy: cs)
        let component = filtered.joined(separator: "")
        let isNumeric = string == component

        // Then if the replacement string's numeric, or if it's
        // a backspace, or if it's a decimal point and the text
        // field doesn't already contain a decimal point,
        // reformat the new complete number using
        if isNumeric {
            let formatter = NumberFormatter()
            formatter.numberStyle = .decimal
            formatter.maximumFractionDigits = 8
            // Combine the new text with the old; then remove any
            // commas from the textField before formatting
            let newString = (textField.text! as NSString).replacingCharacters(in: range, with: string)
            let numberWithOutCommas = newString.replacingOccurrences(of: ",", with: "")
            let number = formatter.number(from: numberWithOutCommas)
            if number != nil {
                var formattedString = formatter.string(from: number!)
                // If the last entry was a decimal or a zero after a decimal,
                // re-add it here because the formatter will naturally remove
                // it.
                if string == "." && range.location == textField.text?.count {
                    formattedString = formattedString?.appending(".")
                }
                textField.text = formattedString
            } else {
                textField.text = nil
            }
        }
        return false

    }
Kirill
  • 738
  • 10
  • 26
Ajay Singh Mehra
  • 1,313
  • 9
  • 19
  • This is the best answer. Thank you. – Sipho Koza Oct 16 '18 at 11:31
  • Quick question - How do you restrict a user to enter 5 digits before the dot (.) and only three digits after the dot(.) – Sipho Koza Nov 12 '18 at 12:20
  • @SiphoKoza try to set `formattor.maximumIntegerDigits` to 5 and `formatter.maximumFractionDigits` to 3 – Ajay Singh Mehra Nov 13 '18 at 06:03
  • @AjaySinghMehra `formatter.maximumIntegerDigits = 5` is not working as expected. It works for `11111` but I am able to type `1112223334444555667789`. Any way to make it work so that I can put just 5 digits before decimal? – Mamta Nov 15 '18 at 13:21
  • I need to use .currency format due to locale issues, any solution to that? – Mamta Apr 17 '19 at 14:12
  • Great answer. Please remember that `let numberWithOutCommas = newString.replacingOccurrences(of: ",", with: "")` might not work in some localizations. E.g. in Russia white spaces are being using instead of commas. – Kirill Jan 02 '20 at 01:31
3

Format the number with grouping attributes as shown here.

NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setGroupingSeparator:@","];
[numberFormatter setGroupingSize:3];
[numberFormatter setDecimalSeparator:@"."];
[numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
[numberFormatter setMaximumFractionDigits:3];
[numberFormatter setMinimumFractionDigits:3];

Output for the above code is

1,234,567.850
1

Here is a solution in swift 4.

 @objc func textFieldValDidChange(_ textField: UITextField) {
    let formatter = NumberFormatter()
    formatter.numberStyle = NumberFormatter.Style.decimal
    if textField.text!.count >= 1 {
       let number = Double(bottomView.balanceTxtField.text!.replacingOccurrences(of: ",", with: ""))
        let result = formatter.string(from: NSNumber(value: number!))
        textField.text = result!
    }
}

Don't forget to add editingChanged action as below:

textField.addTarget(self, action:#selector(ViewController.textFieldValDidChange), for: .editingChanged)
Sam
  • 446
  • 7
  • 17
0

Use the UITextFieldDelegate method: (your view controller needs to be a delegate of the textfield)

-(BOOL) textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string

When a character is added to the textField, it will call this method. You can then insert commas wherever you want.

Note: people can paste text into textfields, delete values, move the cursor, etc. so you have a lot of tests to consider.

There are lots of similar questions on SO, e.g. How to put comma and decimals in my UITextField dynamically?

Auto suggest in UITextfield with comma separation

etc

Community
  • 1
  • 1
shim
  • 9,289
  • 12
  • 69
  • 108
0

Windy please keep in mind the commas should get add to the number itself, not like user has to enter them.

First

// Add a "textFieldDidChange" notification method to the text field control.
[textField addTarget:self 
          action:@selector(textFieldDidChange:) 
forControlEvents:UIControlEventEditingChanged];

You have to change to change the Text yourself. And the Code that will add the commas is

-(void) textFieldDidChange {

    NSNumberFormatter *formatter = [NSNumberFormatter new];
    [formatter setNumberStyle:NSNumberFormatterDecimalStyle]; // this line is important!

    NSString *formatted = [formatter stringFromNumber:[NSNumber numberWithInteger:2000000]];
    NSLog(@"the Formatted String is  %@",formatted);

   textField.text = formatted;
}
Jasmeet
  • 1,522
  • 2
  • 22
  • 41
0

EDIT See Lindsey Scott's answer for an updated, correct version.

This is based on Lindsey Scott's previous answer, but updated to account for 0's entered after the decimal:

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {

    if (textField == _questionPoolNameTextField) {
        return YES;
    }

    //For 0's after the decimal point:
    if ([string isEqualToString:@"0"] && (0 <= (int)[textField.text rangeOfString:@"."].location)) {
        if ([textField.text rangeOfString:@"."].location < range.location) {
            return YES;
        }
    }

    // First check whether the replacement string's numeric...
    NSCharacterSet *cs = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789"] invertedSet];
    NSString *filtered = [[string componentsSeparatedByCharactersInSet:cs] componentsJoinedByString:@""];
    bool isNumeric = [string isEqualToString:filtered];

    // Then if the replacement string's numeric, or if it's
    // a backspace, or if it's a decimal point and the text
    // field doesn't already contain a decimal point,
    // reformat the new complete number using
    // NSNumberFormatterDecimalStyle
    if (isNumeric ||
        [string isEqualToString:@""] ||
        ([string isEqualToString:@"."] &&
         [textField.text rangeOfString:@"."].location == NSNotFound)) {

            NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
            [numberFormatter setGroupingSeparator:@","];
            [numberFormatter setGroupingSize:3];
            [numberFormatter setDecimalSeparator:@"."];
            [numberFormatter setMaximumFractionDigits:20];
            [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];

            // Combine the new text with the old; then remove any
            // commas from the textField before formatting
            NSString *combinedText = [textField.text stringByReplacingCharactersInRange:range withString:string];
            NSString *numberWithoutCommas = [combinedText stringByReplacingOccurrencesOfString:@"," withString:@""];
            NSNumber *number = [numberFormatter numberFromString:numberWithoutCommas];

            NSString *formattedString = [numberFormatter stringFromNumber:number];

            // If the last entry was a decimal at the end of the
            // re-add it here because the formatter will naturally
            // remove it.
            if ([string isEqualToString:@"."] &&
                range.location == textField.text.length) {
                formattedString = [formattedString stringByAppendingString:@"."];
            }

            textField.text = formattedString;
        }

    // Return no, because either the replacement string is not
    // valid or it is and the textfield has already been updated
    // accordingly
    return NO;
}
Gazzini
  • 728
  • 8
  • 19
  • 1
    FYI instead of down voting an answer and writing a new one using 90% of that person's code, it's better form to update that answer with a fix. I've fixed my answer accordingly. – Lyndsey Scott Mar 16 '15 at 04:44
  • @LyndseyScott I used code from both you and Nagarajan SathishI; thanks for the tip though. Anyways, I just realized that both this and your corrected answer are still wrong. I'll upvote yours if you can account for the situation where there are multiple 0's in a row after the decimal point, and one of those 0's is deleted. Right now, it behaves incorrectly, and I would down vote this answer as well if I could. – Gazzini Mar 16 '15 at 11:33
  • OK, I've updated my answer to add backspaces, `([string isEqualToString:@"0"] || [string isEqualToString:@""])`. But even your answer doesn't deserve a down vote since most of it is correct and still useful. Down votes are meant for answers that are not useful...not useful answers with an error... – Lyndsey Scott Mar 16 '15 at 14:59
  • And even the current update is a temporary fix (I'll revisit this answer later when I have time). Logically speaking, the code to account for 0s really belongs in an else statement after `if ([string isEqualToString:@"."] && range.location == textField.text.length) {`. I wouldn't be surprised if there are more issues with the temporary fix since it ends up completely forgoing the bulk of the code. – Lyndsey Scott Mar 16 '15 at 15:00
  • @LyndseyScott It seems good; if you backspace from behind the decimal, just don't do anything special. Kudos! – Gazzini Mar 16 '15 at 19:51
0

I have another one. Fix some bugs with local configuration and zero after decimal point

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    
    let point = Locale.current.decimalSeparator!
    let decSep = Locale.current.groupingSeparator!
    
    
    let text = textField.text!
    let textRange = Range(range, in: text)!
    
    var fractionLength = 0
    var isRangeUpperPoint = false
    
    if let startPoint = text.lastIndex(of: point.first!) {
        let end = text.endIndex
        let str = String(text[startPoint..<end])
        fractionLength = str.count
        isRangeUpperPoint = textRange.lowerBound >= startPoint
    }
    
    if  fractionLength == 3 && string != "" && isRangeUpperPoint {
        return false
    }
    
    let r = (textField.text! as NSString).range(of: point).location < range.location
    if (string == "0" || string == "") && r {
        return true
    }
    
    // First check whether the replacement string's numeric...
    let cs = NSCharacterSet(charactersIn: "0123456789\(point)").inverted
    let filtered = string.components(separatedBy: cs)
    let component = filtered.joined(separator: "")
    let isNumeric = string == component
    
    
    if isNumeric {
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        formatter.maximumFractionDigits = 2
        // Combine the new text with the old; then remove any
        // commas from the textField before formatting
        
        
        let newString = text.replacingCharacters(in: textRange,  with: string)
        
        
        let numberWithOutCommas = newString.replacingOccurrences(of: decSep, with: "")
        let number = formatter.number(from: numberWithOutCommas)
        if number != nil {
            var formattedString = formatter.string(from: number!)
            // If the last entry was a decimal or a zero after a decimal,
            // re-add it here because the formatter will naturally remove
            // it.
            if string == point && range.location == textField.text?.count {
                formattedString = formattedString?.appending(point)
            }
            textField.text = formattedString
        } else {
            textField.text = nil
        }
    }
    
    
    
   return false
    
}
Leonif
  • 466
  • 4
  • 17