51

In my app users need to be able to enter numeric values with decimal places. The iPhone doesn't provides a keyboard that's specific for this purpose - only a number pad and a keyboard with numbers and symbols.

Is there an easy way to use the latter and prevent any non-numeric input from being entered without having to regex the final result?

Thanks!

CVertex
  • 17,997
  • 28
  • 94
  • 124
Andrew Grant
  • 58,260
  • 22
  • 130
  • 143

11 Answers11

53

I think it would be good to point out that as of iOS 4.1 you can use the new UIKeyboardTypeDecimalPad.

So now you just have to:

myTextField.keyboardType=UIKeyboardTypeDecimalPad;
Defragged
  • 1,914
  • 15
  • 15
Zebs
  • 5,378
  • 2
  • 35
  • 49
50

A more elegant solution happens to also be the simplest.

You don't need a decimal separator key

Why? Because you can simply infer it from the user's input. For instance, in the US locale when you what to enter in $1.23, you start by entering the numbers 1-2-3 (in that order). In the system, as each character is entered, this would be recognized as:

  • user enters 1: $0.01
  • user enters 2: $0.12
  • user enters 3: $1.23

Notice how we inferred the decimal separator based on the user's input. Now, if the user wants to enter in $1.00, they would simply enter the numbers 1-0-0.

In order for your code to handle currencies of a different locale, you need to get the maximum fraction digits of the currency. This can be done with the following code snippet:

NSNumberFormatter *currencyFormatter = [[NSNumberFormatter alloc] init];
[currencyFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
int currencyScale = [currencyFormatter maximumFractionDigits];

For example, the Japanese yen has a maximum fraction digit of 0. So, when dealing with yen input, there is no decimal separator and thus no need to even worry about fractional amounts.

This approach to the problem allows you to use the stock numeric input keypad provided by Apple without the headaches of custom keypads, regex validation, etc.

shek
  • 12,721
  • 2
  • 26
  • 23
  • 7
    Just to make sure -- don't use floats for currency! – Svante Feb 24 '09 at 01:16
  • 1
    I also wrote a blog post concerning this for anyone that is interested: http://blog.weareuproar.com/?p=11 – shek Mar 12 '09 at 03:57
  • 2
    One point - unfortunately the iPhone SDK UIControl does not support a setFormatter method to allow you to attach the NSFormatter to the UITextField, or other UIControl (the equivalent Cocoa NSControl does support setFormatter). I raised an enhancement issue with Apple about this (#5847381). – Dan J Oct 23 '09 at 15:09
  • 19
    The original question doesn't say anything about currency, yet this answer pretty much assumes we want to input currency. It's not helpful if you want to accept arbitrary floating point input. – benzado Jan 10 '10 at 00:21
  • Given this answer was accepted I suppose the original question was referring to decimal numbers where the precision is fixed (of which currency is just one example). – shek Jan 10 '10 at 03:50
  • 10
    As a user, I find it rather irritating to have to enter 100 if I want to enter a generic fractional value or a currency value. E.g, for a currency, if the app prompts me to enter an amount, I'd expect to be entering it in dollars or euros or yen, not in cents (i.e. 100th of a dollar or euro). Hence, I'd expect to be able to enter "1" to mean $1. Your suggestion forces the user to double check that entering "1" actually didn't enter $1 but $.01. This is not what the user would expect, usually. – Thomas Tempelmann May 11 '11 at 10:33
  • But the numeric keyboard doesn't have a "Done" button and there's no way to dismiss it! – Van Du Tran Mar 09 '12 at 23:20
  • I think this answer would have been a lot better if it'd included a little code. – drewish Jul 23 '12 at 01:00
  • 1
    The question doesn't state this is only for currency? This method doesn't work if the number of digits after the decimal point is flexible/unknown. – Jonathan. Aug 15 '12 at 18:15
12

Here is an example for the solution suggested in the accepted answer. This doesn't handle other currencies or anything - in my case I only needed support for dollars, no matter what the locale/currency so this was OK for me:

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

    double currentValue = [textField.text doubleValue];
    //Replace line above with this
    //double currentValue = [[textField text] substringFromIndex:1] doubleValue];
    double cents = round(currentValue * 100.0f);

    if ([string length]) {
        for (size_t i = 0; i < [string length]; i++) {
            unichar c = [string characterAtIndex:i];
            if (isnumber(c)) {
                cents *= 10;
                cents += c - '0'; 
            }            
        }
    } else {
        // back Space
        cents = floor(cents / 10);
    }

    textField.text = [NSString stringWithFormat:@"%.2f", cents / 100.0f];
    //Add this line
    //[textField setText:[NSString stringWithFormat:@"$%@",[textField text]]];
    return NO;
}

The rounds and floors are important a) because of the floating-point representation sometimes losing .00001 or whatever and b) the format string rounding up any precision we deleted in the backspace part.

Mason
  • 6,893
  • 15
  • 71
  • 115
Mike Weller
  • 45,401
  • 15
  • 131
  • 151
  • this piece of code doesn't seem to work after entering the 15th number. I entered 999999999999999 and then on the next 9, the output was 100000000000000.00 – Joo Park Apr 18 '10 at 06:33
  • 1
    You are losing precision because it uses floating point numbers. – Mike Weller Apr 19 '10 at 08:02
  • 1
    I have slightly modified your algorithm so that it deposits fractions of a penny into my offshore account. It is working already. MUA HA HA. – ZaBlanc Jul 07 '10 at 21:21
3

I wanted to do exactly the same thing, except with currencies rather than straight decimal values.

I ended up creating a custom view which contains a UITextField and a UILabel. The label covers the text field, but the text field still receives touches. I use the UITextFieldTextDidChange notification to observe changes in the text field (and I used a NSNumberFormatter to turn the resulting number into a formatted currency value) to update the label.

To disable the loupe that allows the user to reposition the insertion point, you'll need to use a custom UITextField subclass and override touchesBegan:withEvent: and set it to do nothing.

My solution might be different from what you need because the decimal point is always fixed -- I use the system's currency setting to determine how many there digits ought to be after the decimal point. However, the numeric keypad doesn't have a decimal point on it. And you can't add any buttons to the keyboard (which is especially aggravating because there's a blank button in the lower-left corner of the keyboard that would be perfect for a decimal point!) So I don't have a solution for that, unfortunately.

Alex
  • 26,829
  • 3
  • 55
  • 74
  • I've done something like this, except with a custom view. When it receives a touch I set up a text field at the bottom of the screen (behind the keyboard) and call `becomeFirstResponder` on it. Then I don't need to worry about disabling the loupe. – benzado Jan 10 '10 at 00:24
  • What did you do about a cursor? – drewish Aug 24 '12 at 02:01
2

Depending on the specific application, providing a slider that the user can select a position from might be a better choice on the iphone. Then no digits need to be entered at all.

Martin v. Löwis
  • 124,830
  • 17
  • 198
  • 235
2

You may want to use a slider (as suggested by Martin v. Löwis) or a UIPickerView with a separate wheel for each of the digits.

Jeroen Heijmans
  • 4,546
  • 2
  • 17
  • 16
2

I built a custom Number pad view controller with decimal point... check it out:

http://sites.google.com/site/psychopupnet/iphone-sdk/tenkeypadcustom10-keypadwithdecimal

Richard
  • 1,677
  • 3
  • 13
  • 15
1

As of iOS4.1, there is a new keyboard type UIKeyboardTypeNumberPad, but unfortunately, as of yet it doesn't seem to appear in the Interface Builder pick list.

Screwtape
  • 11
  • 1
1

Here's how to do it without using floats, round() or ceil() in a currency agnostic manner.

In you view controller, set up the following instance variables (with associated @property statements if that's your bag):

@interface MyViewController : UIViewController <UITextFieldDelegate> {
@private
    UITextField *firstResponder;
    NSNumberFormatter *formatter;
    NSInteger currencyScale;
    NSString *enteredDigits;
}

@property (nonatomic, readwrite, assign) UITextField *firstResponder;
@property (nonatomic, readwrite, retain) NSNumberFormatter *formatter;
@property (nonatomic, readwrite, retain) NSString *enteredDigits;

@end

and your viewDidLoad method should contain the following:

- (void)viewDidLoad {
    [super viewDidLoad];

    NSNumberFormatter *aFormatter = [[NSNumberFormatter alloc] init];
    [aFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
    currencyScale = -1 * [aFormatter maximumFractionDigits];
    self.formatter = aFormatter;
    [aFormatter release];
}

Then implement your UITextFieldDelegate methods as follows:

#pragma mark -
#pragma mark UITextFieldDelegate methods

- (void)textFieldDidBeginEditing:(UITextField *)textField {
    // Keep a pointer to the field, so we can resign it from a toolbar
    self.firstResponder = textField;
    self.enteredDigits = @"";
}

- (void)textFieldDidEndEditing:(UITextField *)textField {
    if ([self.enteredDigits length] > 0) {
        // Get the amount
        NSDecimalNumber *result = [[NSDecimalNumber decimalNumberWithString:self.enteredDigits] decimalNumberByMultiplyingByPowerOf10:currencyScale];
        NSLog(@"result: %@", result);
    }
}

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

    // Check the length of the string
    if ([string length]) {
        self.enteredDigits = [self.enteredDigits stringByAppendingFormat:@"%d", [string integerValue]];
    } else {
        // This is a backspace
        NSUInteger len = [self.enteredDigits length];
        if (len > 1) {
            self.enteredDigits = [self.enteredDigits substringWithRange:NSMakeRange(0, len - 1)];
        } else {
            self.enteredDigits = @"";
        }
    }

    NSDecimalNumber *decimal = nil;

    if ( ![self.enteredDigits isEqualToString:@""]) {
        decimal = [[NSDecimalNumber decimalNumberWithString:self.enteredDigits] decimalNumberByMultiplyingByPowerOf10:currencyScale];
    } else {
        decimal = [NSDecimalNumber zero];
    }

    // Replace the text with the localized decimal number
    textField.text = [self.formatter stringFromNumber:decimal];

    return NO;  
}

Only tested this with pounds and pence, but it should work with Japanese Yen too. If you want to format decimals for non-currency purposes, then just read the documentation on NSNumberFormatter and set whatever format/maximumFractionDigits you want.

Daniel Thorpe
  • 3,911
  • 2
  • 28
  • 28
  • get rid of the leak at `self.formatter = [[NSNumberFormatter alloc] init];` – Matthias Bauch Feb 08 '11 at 10:52
  • This seems to work. But after the 15th digit it gets rounded. I'm starting to think the real solution is to never convert the string. But just add the formatting. – Hackmodford Jan 06 '12 at 14:56
  • 1
    I just limited the amount of digits that can be added. This works perfectly. Also a good tip: If you are pre-populating the UITextfield you need to set the enteredDigits string to reflect what you added. Then everything works fine. Also I only needed the textField should changeCharectersInRange delegate method to make this work. – Hackmodford Jan 06 '12 at 18:07
0

A Swift 2 implementation of Mike Weller's post, also only USD:

 func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    guard let str = textField.text else {
        textField.text = "0.00"
        return false
    }

    let value = (str as NSString).doubleValue

    var cents = round(value * 100)
    if string.characters.count > 0 {
        for c in string.characters {
            if let num = Int(String(c)) {
                cents *= 10
                cents += Double(num)
            }
        }
    }
    else {
        cents = floor(cents / 10)
    }

    textField.text = NSString(format: "%.2f", cents/100) as String

    return false
}
chasew
  • 8,438
  • 7
  • 41
  • 48
0

You can use STATextField and set currencyRepresentation to YES which:

Ensures no more than one decimal point is entered and that no more than 2 digits are entered to the right of said decimal point.

There's also STAATMTextField which supports currency mode and ATM text entry by default:

Provides a text field that mimics ATM machine input behavior.

Stunner
  • 12,025
  • 12
  • 86
  • 145