Can Aksoy Can Aksoy - 4 months ago 373x
iOS Question

Formatting a UITextField for credit card input like (xxxx xxxx xxxx xxxx)

I want to format a

for entering a credit card number into such that it only allows digits to be entered and automatically inserts spaces so that the number is formatted like so:


How can I do this?


First of all, are you sure you want to do this? While the XXXX XXXX XXXX XXXX format for credit and debit card numbers is the most common one, it's not the only one. For instance, American Express cards have 15 digit numbers usually written in XXXX XXXXXX XXXXX format, like this:

An American Express card

Even Visa cards can have fewer than 16 digits, and Maestro cards can have more:

A Russian Maestro card with 18 digits

You may want to consider which card networks (like Visa, Maestro, etc.) you're supporting and what country or countries your app will be used in, and try to find out what card number formats issuers in those countries typically use.

You've done that, or you're already sure you're doing the right thing? Alright then...

The Short Answer

Just do two things.

Firstly, to your UITextFieldDelegate, add these instance variables...

NSString *previousTextFieldContent;
UITextRange *previousSelection;

... and these methods:

// Version 1.2
// Source and explanation:
-(void)reformatAsCardNumber:(UITextField *)textField
    // In order to make the cursor end up positioned correctly, we need to
    // explicitly reposition it after we inject spaces into the text.
    // targetCursorPosition keeps track of where the cursor needs to end up as
    // we modify the string, and at the end we set the cursor position to it.
    NSUInteger targetCursorPosition = 
        [textField offsetFromPosition:textField.beginningOfDocument

    NSString *cardNumberWithoutSpaces = 
        [self removeNonDigits:textField.text

    if ([cardNumberWithoutSpaces length] > 19) {
        // If the user is trying to enter more than 19 digits, we prevent 
        // their change, leaving the text field in  its previous state.
        // While 16 digits is usual, credit card numbers have a hard 
        // maximum of 19 digits defined by ISO standard 7812-1 in section
        // 3.8 and elsewhere. Applying this hard maximum here rather than
        // a maximum of 16 ensures that users with unusual card numbers
        // will still be able to enter their card number even if the
        // resultant formatting is odd.
        [textField setText:previousTextFieldContent];
        textField.selectedTextRange = previousSelection;

    NSString *cardNumberWithSpaces = 
        [self insertSpacesEveryFourDigitsIntoString:cardNumberWithoutSpaces

    textField.text = cardNumberWithSpaces;
    UITextPosition *targetPosition = 
        [textField positionFromPosition:[textField beginningOfDocument]

    [textField setSelectedTextRange:
        [textField textRangeFromPosition:targetPosition

-(BOOL)textField:(UITextField *)textField 
                     replacementString:(NSString *)string
    // Note textField's current state before performing the change, in case
    // reformatTextField wants to revert it
    previousTextFieldContent = textField.text;
    previousSelection = textField.selectedTextRange;

    return YES;

 Removes non-digits from the string, decrementing `cursorPosition` as
 appropriate so that, for instance, if we pass in `@"1111 1123 1111"`
 and a cursor position of `8`, the cursor position will be changed to
 `7` (keeping it between the '2' and the '3' after the spaces are removed).
- (NSString *)removeNonDigits:(NSString *)string
                andPreserveCursorPosition:(NSUInteger *)cursorPosition 
    NSUInteger originalCursorPosition = *cursorPosition;
    NSMutableString *digitsOnlyString = [NSMutableString new];
    for (NSUInteger i=0; i<[string length]; i++) {
        unichar characterToAdd = [string characterAtIndex:i];
        if (isdigit(characterToAdd)) {
            NSString *stringToAdd = 
                [NSString stringWithCharacters:&characterToAdd

            [digitsOnlyString appendString:stringToAdd];
        else {
            if (i < originalCursorPosition) {

    return digitsOnlyString;

 Inserts spaces into the string to format it as a credit card number,
 incrementing `cursorPosition` as appropriate so that, for instance, if we
 pass in `@"111111231111"` and a cursor position of `7`, the cursor position
 will be changed to `8` (keeping it between the '2' and the '3' after the
 spaces are added).
- (NSString *)insertSpacesEveryFourDigitsIntoString:(NSString *)string
              andPreserveCursorPosition:(NSUInteger *)cursorPosition 
    NSMutableString *stringWithAddedSpaces = [NSMutableString new];
    NSUInteger cursorPositionInSpacelessString = *cursorPosition;
    for (NSUInteger i=0; i<[string length]; i++) {
        if ((i>0) && ((i % 4) == 0)) {
            [stringWithAddedSpaces appendString:@" "];
            if (i < cursorPositionInSpacelessString) {
        unichar characterToAdd = [string characterAtIndex:i];
        NSString *stringToAdd = 
            [NSString stringWithCharacters:&characterToAdd length:1];

        [stringWithAddedSpaces appendString:stringToAdd];

    return stringWithAddedSpaces;

Secondly, set reformatCardNumber: to be called whenever the text field fires a UIControlEventEditingChanged event:

[yourTextField addTarget:yourTextFieldDelegate 

(Of course, you'll need to do this at some point after your text field and its delegate have been instantiated. If you're using storyboards, the viewDidLoad method of your view controller is an appropriate place.

Some Explanation

This is a deceptively complicated problem. Two important issues that may not be immediately obvious (both of the previous answers posted fail to take these into account):

  1. There are more ways for the user to interact with a text field than just typing in single characters at the end of their existing input. You also have to properly handle the user adding characters in the middle of the string, deleting single characters, deleting multiple selected characters, and pasting in multiple characters. Some simpler/more naive approaches to this problem will fail to handle some of these interactions properly. The most perverse case is a user pasting in multiple characters in the middle of the string to replace other characters, and this solution is general enough to handle that.
  2. You don't just need to reformat the text of the text field properly after the user modifies it - you also need to position the text cursor sensibly. Naive approaches to the problem that don't take this into account will almost certainly end up doing something silly with the text cursor in some cases (like putting it to the end of the text field after the user adds a digit in the middle of it).

There's one other trap lurking here as well, which is far less important than the above two but irritating to work around:

  • Returning NO from textField: shouldChangeCharactersInRange: replacementString breaks the 'Cut' button the user gets when they select text in the text field, which is why I don't do it. Returning NO from that method results in 'Cut' simply not updating the clipboard at all, and I know of no fix or workaround. As a result, we need to do the reformatting of the text field in a UIControlEventEditingChanged handler instead of (more obviously) in shouldChangeCharactersInRange: itself.

    Luckily, the UIControl event handlers seem to get called before UI updates get flushed to the screen, so this approach works fine.

The simplest and easiest way to deal with issue #1 (and the way used in the code above) is to strip out all spaces and reinsert them in the correct positions every time the content of the text field changes, sparing us the need to figure out what kind of text manipulation (an insertion, a deletion, or a replacement) is going on and handle the possibilities differently.

To deal with issue #2, we keep track of how the desired index of the cursor changes as we strip out non-digits and then insert spaces. This is why the code rather verbosely performs these manipulations character-by-character using NSMutableString, rather than using NSString's string replacement methods.

There are also a whole bunch of minor questions about exactly how the text field should behave that don't have obvious correct answers:

  • If the user tries to paste in something that would cause the content of the text field to exceed 19 digits, should the beginning of the pasted string be inserted (until 19 digits are reached) and the remainder cropped, or should nothing be inserted at all?
  • If the user tries to delete a single space by positioning their cursor after it and pressing the backspace key, should nothing happen and the cursor remain where it is, should the cursor move left one character (placing it before the space), or should the digit to the left of the space be deleted as though the cursor were already left of the space?
  • When the user types in the fourth, eighth, or twelfth digit, should a space be immediately inserted and the cursor moved after it, or should the space only be inserted after the user types the fifth, ninth, or thirteenth digit?
  • When the user deletes the first digit after a space, if this doesn't cause the space to be removed entirely, should this lead to their cursor being positioned before or after the space?

Probably any answer to any of these questions will be adequate, but I list them just to make clear that there are actually a lot of special cases that you might want to think carefully about here, if you were obsessive enough. In the code above, I've picked answers to these questions that seemed reasonable to me. If you happen to have strong feelings about any of these points that aren't compatible with the way my code behaves, it should be easy enough to tweak it to your needs.