Reputation: 41
After over a day of poking around with this problem I will see if I can get some help. This question has been more or less asked before, but it seems no one is giving a full answer so hopefully we can get it now.
Using a UILabel and a UITextView (w/ number keyboard) I want to achieve an ATM like behavior of letting the users just type the numbers and it is formatted as currency in the label. The idea is basically outlined here:
What is the best way to enter numeric values with decimal points?
The only issue is that it never explicitly says how we can go from having an integer like 123 in the textfield and displaying in the label as $1.23 or 123¥ etc. Anyone have code that does this?
Upvotes: 0
Views: 3958
Reputation: 9380
I didn't really like the existing answers here so I combined a couple of techniques. I used a hidden UITextField with the number pad keyboard for input, and a visible UILabel for formatting.
I've got to properties that hold on to everything:
@property (weak, nonatomic) IBOutlet UILabel *amountLabel;
@property (weak, nonatomic) IBOutlet UITextField *amountText;
@property (retain, nonatomic) NSDecimalNumber *amount;
I've got the amount and a NSNumberFormatter as ivars:
NSDecimalNumber *amount_;
NSNumberFormatter *formatter;
I setup my formatter at init:
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
// Custom initialization
formatter = [NSNumberFormatter new];
formatter.numberStyle = NSNumberFormatterCurrencyStyle;
}
return self;
}
Here's the code I'm using to validate the input convert it to
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
NSString *asText = [textField.text stringByReplacingCharactersInRange:range withString:string];
if ([asText length] == 0) {
[self setAmount:[NSDecimalNumber zero]];
return YES;
}
// We just want digits so cast the string to an integer then compare it
// to itself. If it's unchanged then it's workable.
NSInteger asInteger = [asText integerValue];
NSNumber *asNumber = [NSNumber numberWithInteger:asInteger];
if ([[asNumber stringValue] isEqualToString:asText]) {
// Convert it to a decimal and shift it over by the fractional part.
NSDecimalNumber *newAmount = [NSDecimalNumber decimalNumberWithDecimal:[asNumber decimalValue]];
[self setAmount:[newAmount decimalNumberByMultiplyingByPowerOf10:-formatter.maximumFractionDigits]];
return YES;
}
return NO;
}
I've got this setter than handles formatting the label and enabling the done button:
-(void)setAmount:(NSDecimalNumber *)amount
{
amount_ = amount;
amountLabel.text = [formatter stringFromNumber:amount];
self.navigationItem.rightBarButtonItem.enabled = [self isValid];
}
Upvotes: 0
Reputation: 3455
Since I still didn't see correct Answers to this question I will share my solution without using the NSScanner (the scanner doesn't seem to work for me). Is's a combination out of this " What is the best way to enter numeric values with decimal points? " and this " Remove all but numbers from NSString " answers.
First I present a NSString with the users local currency settings in a UITextField like this:
//currencyFormatter is of type NSNumberFormatter
if (currencyFormatter == nil) {
currencyFormatter = [[NSNumberFormatter alloc] init];
[currencyFormatter setLocale:[NSLocale currentLocale]];
[currencyFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
//[currencyFormatter setGeneratesDecimalNumbers:YES];
decimalSeperator = [currencyFormatter decimalSeparator]; //NSString
currencyScale = [currencyFormatter maximumFractionDigits]; //short
//[currencyFormatter release]; don't forget to release the Formatter at one point
}
//costField is of type UITextField
NSDecimalNumber *nullValue = [NSDecimalNumber decimalNumberWithMantissa:0 exponent:currencyScale isNegative:NO];
[costField setText:[currencyFormatter stringFromNumber:nullValue]];
You might do this in the viewControllers method viewDidLoad:. Depending on the users settings there will be displayed a string like this: $0.00 (for local settings United Stated). Depending on your situation here you might want to present a value out of your data model.
When the user touches inside the text field I will present a Keyboard with type:
costField.keyboardType = UIKeyboardTypeDecimalPad;
This prevents the user to enter anything else but digits.
In the following UITextField's delegate method I separate the string to get only the numbers (here I avoid using the NSScanner). This is possible, because I know where to set the decimal separator by using the before specified 'currencyScale' value:
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range
replacementString:(NSString *)string {
if (textField == costField) {
//if for what ever reason ther currency scale is not available set it to 2
//which is the most common scale value
if (!currencyScale) {
currencyScale = 2;
}
// separate string from all but numbers
// https://stackoverflow.com/questions/1129521/remove-all-but-numbers-from-nsstring/1163595#1163595
NSString *aString = [textField text];
NSMutableString *strippedString = [NSMutableString stringWithCapacity:10];
for (int i=0; i<[aString length]; i++) {
if (isdigit([aString characterAtIndex:i])) {
[strippedString appendFormat:@"%c",[aString characterAtIndex:i]];
}
}
//add the newly entered character as a number
// https://stackoverflow.com/questions/276382/what-is-the-best-way-to-enter-numeric-values-with-decimal-points/2636699#2636699
double cents = [strippedString doubleValue];
NSLog(@"Cents:%f ",[strippedString doubleValue]);
if ([string length]) {
for (size_t i = 0; i < [string length]; i++) {
unichar c = [string characterAtIndex:i];
if (isnumber(c)) {
cents *= 10; //multiply by 10 to add a 0 at the end
cents += c - '0'; // makes a number out of the charactor and replace the 0 (see ASCII Table)
}
}
}
else {
// back Space if the user delete a number
cents = floor(cents / 10);
}
//like this you could save the value as a NSDecimalNumber in your data model
//costPerHour is of type NSDecimalNumber
self.costPerHour = [NSDecimalNumber decimalNumberWithMantissa:cents exponent:-currencyScale isNegative:NO];
//creat the string with the currency symbol and the currency separator
[textField setText:[currencyFormatter stringFromNumber:costPerHour]];
return NO;
}
return YES;
}
In this way the currency entered by the user will always be correct and there is no need to check it. No matter which currency settings is selected, this will always result to be the correctly formatted currency.
Upvotes: 0
Reputation: 41
I have found a solution, and as per the purpose of this question I am going to provide a complete answer for those who have this problem in the future. First I created a new Helper Class called NumberFormatting and created two methods.
//
// NumberFormatting.h
// Created by Noah Hendrix on 12/26/09.
//
#import <Foundation/Foundation.h>
@interface NumberFormatting : NSObject {
}
-(NSString *)stringToCurrency:(NSString *)aString;
-(NSString *)decimalToIntString:(NSDecimalNumber *)aDecimal;
@end
and here is the implementation file:
//
// NumberFormatting.m
// Created by Noah Hendrix on 12/26/09.
//
#import "NumberFormatting.h"
@implementation NumberFormatting
-(NSString *)stringToCurrency:(NSString *)aString {
NSNumberFormatter *currencyFormatter = [[NSNumberFormatter alloc] init];
[currencyFormatter setGeneratesDecimalNumbers:YES];
[currencyFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
if ([aString length] == 0)
aString = @"0";
//convert the integer value of the price to a decimal number i.e. 123 = 1.23
//[currencyFormatter maximumFractionDigits] gives number of decimal places we need to have
//multiply by -1 so the decimal moves inward
//we are only dealing with positive values so the number is not negative
NSDecimalNumber *value = [NSDecimalNumber decimalNumberWithMantissa:[aString integerValue]
exponent:(-1 * [currencyFormatter maximumFractionDigits])
isNegative:NO];
return [currencyFormatter stringFromNumber:value];
}
-(NSString *)decimalToIntString:(NSDecimalNumber *)aDecimal {
NSNumberFormatter *currencyFormatter = [[NSNumberFormatter alloc] init];
[currencyFormatter setGeneratesDecimalNumbers:YES];
[currencyFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
if (aDecimal == nil)
aDecimal = [NSDecimalNumber zero];
NSDecimalNumber *price = [NSDecimalNumber decimalNumberWithMantissa:[aDecimal integerValue]
exponent:([currencyFormatter maximumFractionDigits])
isNegative:NO];
return [price stringValue];
}
@end
The first method, stringToCurrency, will take an integer number (passed in from a textfield in this case) and convert it to a decimal value using moving the decimal point as appropriate for the users locale settings. It then returns a string representation formatted as currency using NSNumberFormatter.
The second method does the reverse it takes a value like 1.23 and converts it back to 123 using a similar method.
Here is an example of how I used it
...
self.accountBalanceCell.textField.text = [[NumberFormatting alloc] decimalToIntString:account.accountBalance];
...
[self.accountBalanceCell.textField addTarget:self
action:@selector(updateBalance:)
forControlEvents:UIControlEventEditingChanged];
Here we set the value of the text field to the decimal value from the data store and then we set a observer to watch for changes to the text field and run the method updateBalance
- (void)updateBalance:(id)sender {
UILabel *balanceLabel = (UILabel *)[accountBalanceCell.contentView viewWithTag:1000];
NSString *value = ((UITextField *)sender).text;
balanceLabel.text = [[NumberFormatting alloc] stringToCurrency:value];
}
Which simply takes the textfield value and run it through the stringToCurrency method described above.
To me this seems hackish so please take the a moment to look over and clean it up if you are interested in using it. Also I notice for large values it breaks.
Upvotes: 3
Reputation: 96966
Take a look at NSNumberFormatter
, which will format numerical data based on the current or specified locale.
Upvotes: 2