iOS Number Entry Validation

It is important to validate data entered by users both from a programming point of view and a good user experience. If we do not validate the values then we have to handle invalid data in logic later on, which is often more work, to avoid bugs and issues. For the end user this will impact them when using the app as they will then have navigate back from what they are now doing in order to edit and correct the invalid data. Validating the value at the point of entry and giving meaningful error messages also helps the user by cutting down on simple typing mistakes.

For a recent project we wanted to allow the user to define the kind of numbers that that wanted to keep track of. For example, they might want to track ‘Sales’ in USD (Dollars and cents) or GBP (Pounds and pence) and so want to allow numbers with 2 decimal places (fraction digits) and not allow negative values. Alternatively they might want to track ‘Stock’ and so only allow integer values.

When displaying or entering numbers in iOS we can make use of NSNumberFormatter to handle the conversion between NSNumber (or NSDecimalNumber) instances and NSString instances and back again.

So, how do we validate that the user has only entered a value with 2 decimal places. Well the NSNumberFormatter documentation says:

setMaximumFractionDigits:
Sets the maximum number of digits after the decimal separator allowed as input by the receiver.
- (void)setMaximumFractionDigits:(NSUInteger)number
Parameters
number
The maximum number of digits after the decimal separator allowed as input.

Easy, we just set up the NSNumberFormatter and it handles it for us. However, a bit of testing and some checking with Apple Technical Support later it appears that this only applies for the conversion of numbers into text. The documents do not make it clear that in this case ‘input’ specifically means an NSNumber and that this setting is not used when converting in the other direction. I have filed a Bug Report to get the docs on this made clearer.

NSNumberFormatter does a good job of the conversion from numbers to text and gives us a lot of control of the format of the output text. However, when converting from text to a number it handles the conversion, but does not give us any control of the required format for the number. If we have constraints on the format then the validation of these is up to us to enforce. The first thing to do is to make sure that we use the correct method to do the conversion:

-getObjectValue:forString:errorDescription:

Wait a minute! Where does this method appear on NSNumberFormatter? Well it is defined on the superclass NSNumberFormatter and if we look at the definition of this method in the docs we see an important note:

Special Considerations
Prior to Mac OS X v10.6, the implementation of this method in both NSNumberFormatter and NSDateFormatter would return YES and an object value even if only part of the string could be parsed. This is problematic because you cannot be sure what portion of the string was parsed. For applications linked on or after Mac OS X v10.6, this method instead returns an error if part of the string cannot be parsed. You can use getObjectValue:forString:range:error: to get the old behavior—it returns the range of the substring that was successfully parsed.

So, if we use the method:

-getObjectValue:forString:range:error:

which is defined on NSNumberFormatter we will get the old behaviour whereby if only part of the entered text is valid we still get back a number. For example a user could enter ‘123ab.12’ and we would be given back the number 123. For our app we decided that it was quite likely in this case that the user had mistyped when entering the value and we should treat this as an error and highlight it to the user. So we wanted to make use of the new behaviour for the formatter.

To enforce validation as part of the conversion of text to numbers we can subclass NSNumberFormatter and override:

-getObjectValue:forString:errorDescription:

so that it performs our required validation.

First, we call the super implementation in order to get it to convert the text to a number for us. It is then up to us to work out a way to validate the number. So for example to validate the number of decimal places I use an NSDecimalNumberHandler to round the number and then compare it to see if it has changed like so:

NSDecimalNumberHandler *handler = [[NSDecimalNumberHandler alloc]
        initWithRoundingMode:NSRoundPlain scale:[self maximumFractionDigits]
        raiseOnExactness:NO
        raiseOnOverflow:NO
        raiseOnUnderflow:NO
        raiseOnDivideByZero:NO];
NSDecimalNumber *decNumRounded = [decNum decimalNumberByRoundingAccordingToBehavior:handler];
if (![decNum isEqualToNumber:decNumRounded]) {
       // not valid – return appropriate value and error description
}

If rounding the number has changed it, in this case truncated it, then we know that it had too many decimal places and so is not valid.

Notice that we just use the maximumFractionDigits on the number formatter to specify the allowed number of digits rather than add another property of our own. Also bear in mind that the code fragment is written assuming that we are using NSDecimalNumbers. For reasons why you might want to do this read this informative post:Cocoa Tutorial: Don’t Be Lazy With NSDecimalNumber (Like Me).

Created by: 
steve