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

IosObjective CSwiftUitextfieldCredit Card

Ios Problem Overview


I want to format a UITextField 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:

XXXX XXXX XXXX XXXX

How can I do this?

Ios Solutions


Solution 1 - Ios

If you're using Swift, go read my port of this answer for Swift 4 and use that instead.

If you're in Objective-C...

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

NSString *previousTextFieldContent;
UITextRange *previousSelection;

... and these methods:

// Version 1.3
// Source and explanation: http://stackoverflow.com/a/19161529/1709587
-(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                           toPosition:textField.selectedTextRange.start];
    
    NSString *cardNumberWithoutSpaces = 
        [self removeNonDigits:textField.text                  andPreserveCursorPosition:&targetCursorPosition];
    
    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;
        return;
    }
    
    NSString *cardNumberWithSpaces = 
        [self insertCreditCardSpaces:cardNumberWithoutSpaces           andPreserveCursorPosition:&targetCursorPosition];
    
    textField.text = cardNumberWithSpaces;
    UITextPosition *targetPosition = 
        [textField positionFromPosition:[textField beginningOfDocument]
                                 offset:targetCursorPosition];

    [textField setSelectedTextRange:        [textField textRangeFromPosition:targetPosition                              toPosition:targetPosition]
    ];
}

-(BOOL)textField:(UITextField *)textField 
         shouldChangeCharactersInRange:(NSRange)range 
                     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                                        length:1];
            
            [digitsOnlyString appendString:stringToAdd];
        }
        else {
            if (i < originalCursorPosition) {
                (*cursorPosition)--;
            }
        }
    }
    
    return digitsOnlyString;
}

/*
 Detects the card number format from the prefix, then 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 *)insertCreditCardSpaces:(NSString *)string
                          andPreserveCursorPosition:(NSUInteger *)cursorPosition
{
    // Mapping of card prefix to pattern is taken from
    // https://baymard.com/checkout-usability/credit-card-patterns
    
    // UATP cards have 4-5-6 (XXXX-XXXXX-XXXXXX) format
    bool is456 = [string hasPrefix: @"1"];
    
    // These prefixes reliably indicate either a 4-6-5 or 4-6-4 card. We treat all
    // these as 4-6-5-4 to err on the side of always letting the user type more
    // digits.
    bool is465 = [string hasPrefix: @"34"] ||
                 [string hasPrefix: @"37"] ||
    
                 // Diners Club
                 [string hasPrefix: @"300"] ||
                 [string hasPrefix: @"301"] ||
                 [string hasPrefix: @"302"] ||
                 [string hasPrefix: @"303"] ||
                 [string hasPrefix: @"304"] ||
                 [string hasPrefix: @"305"] ||
                 [string hasPrefix: @"309"] ||
                 [string hasPrefix: @"36"] ||
                 [string hasPrefix: @"38"] ||
                 [string hasPrefix: @"39"];
    
    // In all other cases, assume 4-4-4-4-3.
    // This won't always be correct; for instance, Maestro has 4-4-5 cards
    // according to https://baymard.com/checkout-usability/credit-card-patterns,
    // but I don't know what prefixes identify particular formats.
    bool is4444 = !(is456 || is465);
    
    NSMutableString *stringWithAddedSpaces = [NSMutableString new];
    NSUInteger cursorPositionInSpacelessString = *cursorPosition;
    for (NSUInteger i=0; i<[string length]; i++) {
        bool needs465Spacing = (is465 && (i == 4 || i == 10 || i == 15));
        bool needs456Spacing = (is456 && (i == 4 || i == 9 || i == 15));
        bool needs4444Spacing = (is4444 && i > 0 && (i % 4) == 0);
        
        if (needs465Spacing || needs456Spacing || needs4444Spacing) {
            [stringWithAddedSpaces appendString:@" "];
            if (i < cursorPositionInSpacelessString) {
                (*cursorPosition)++;
            }
        }
        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 
                             action:@selector(reformatAsCardNumber:)
                   forControlEvents:UIControlEventEditingChanged];

(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. Three important issues that may not be immediately obvious (and which previous answers here all fail to take into account):

  1. 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 example, 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

  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).

To deal with issue #1, we use the partial mapping of card number prefixes to formats curated by The Baymard Institute at https://baymard.com/checkout-usability/credit-card-patterns. We can automatically detect the the card provider from the first couple of digits and (in some cases) infer the format and adjust our formatting accordingly. Thanks to cnotethegr8 for contributing this idea to this answer.

The simplest and easiest way to deal with issue #2 (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 #3, 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.

Finally, there's one more trap lurking: 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.

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.

Solution 2 - Ios

Below is a working Swift 4 port of Logicopolis's answer (which is in turn a Swift 2 port of an old version of my accepted answer in Objective-C) enhanced with cnotethegr8 's trick for supporting Amex cards and then further enhanced to support more card formats. I suggest looking over the accepted answer if you haven't already, since it helps explain the motivation behind a lot of this code.

Note that the minimal series of steps needed to see this in action is:

  1. Create a new Single View App in Swift.
  2. On Main.storyboard, add a Text Field.
  3. Make the ViewController the delegate of the Text Field.
  4. Paste the code below into ViewController.swift.
  5. Connect the IBOutlet to the Text Field.
  6. Run your app and type in the Text Field.

import UIKit

class ViewController: UIViewController, UITextFieldDelegate {
    private var previousTextFieldContent: String?
    private var previousSelection: UITextRange?
    @IBOutlet var yourTextField: UITextField!;
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib
        yourTextField.addTarget(self, action: #selector(reformatAsCardNumber), for: .editingChanged)
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        previousTextFieldContent = textField.text;
        previousSelection = textField.selectedTextRange;
        return true
    }
    
    @objc func reformatAsCardNumber(textField: UITextField) {
        var targetCursorPosition = 0
        if let startPosition = textField.selectedTextRange?.start {
            targetCursorPosition = textField.offset(from: textField.beginningOfDocument, to: startPosition)
        }
        
        var cardNumberWithoutSpaces = ""
        if let text = textField.text {
            cardNumberWithoutSpaces = self.removeNonDigits(string: text, andPreserveCursorPosition: &targetCursorPosition)
        }
        
        if cardNumberWithoutSpaces.count > 19 {
            textField.text = previousTextFieldContent
            textField.selectedTextRange = previousSelection
            return
        }
        
        let cardNumberWithSpaces = self.insertCreditCardSpaces(cardNumberWithoutSpaces, preserveCursorPosition: &targetCursorPosition)
        textField.text = cardNumberWithSpaces
        
        if let targetPosition = textField.position(from: textField.beginningOfDocument, offset: targetCursorPosition) {
            textField.selectedTextRange = textField.textRange(from: targetPosition, to: targetPosition)
        }
    }
    
    func removeNonDigits(string: String, andPreserveCursorPosition cursorPosition: inout Int) -> String {
        var digitsOnlyString = ""
        let originalCursorPosition = cursorPosition
        
        for i in Swift.stride(from: 0, to: string.count, by: 1) {
            let characterToAdd = string[string.index(string.startIndex, offsetBy: i)]
            if characterToAdd >= "0" && characterToAdd <= "9" {
                digitsOnlyString.append(characterToAdd)
            }
            else if i < originalCursorPosition {
                cursorPosition -= 1
            }
        }
        
        return digitsOnlyString
    }
    
    func insertCreditCardSpaces(_ string: String, preserveCursorPosition cursorPosition: inout Int) -> String {
        // Mapping of card prefix to pattern is taken from
        // https://baymard.com/checkout-usability/credit-card-patterns
        
        // UATP cards have 4-5-6 (XXXX-XXXXX-XXXXXX) format
        let is456 = string.hasPrefix("1")
        
        // These prefixes reliably indicate either a 4-6-5 or 4-6-4 card. We treat all these
        // as 4-6-5-4 to err on the side of always letting the user type more digits.
        let is465 = [
            // Amex
            "34", "37",
            
            // Diners Club
            "300", "301", "302", "303", "304", "305", "309", "36", "38", "39"
        ].contains { string.hasPrefix($0) }
        
        // In all other cases, assume 4-4-4-4-3.
        // This won't always be correct; for instance, Maestro has 4-4-5 cards according
        // to https://baymard.com/checkout-usability/credit-card-patterns, but I don't
        // know what prefixes identify particular formats.
        let is4444 = !(is456 || is465)
        
        var stringWithAddedSpaces = ""
        let cursorPositionInSpacelessString = cursorPosition
        
        for i in 0..<string.count {
            let needs465Spacing = (is465 && (i == 4 || i == 10 || i == 15))
            let needs456Spacing = (is456 && (i == 4 || i == 9 || i == 15))
            let needs4444Spacing = (is4444 && i > 0 && (i % 4) == 0)
            
            if needs465Spacing || needs456Spacing || needs4444Spacing {
                stringWithAddedSpaces.append(" ")
                
                if i < cursorPositionInSpacelessString {
                    cursorPosition += 1
                }
            }
            
            let characterToAdd = string[string.index(string.startIndex, offsetBy:i)]
            stringWithAddedSpaces.append(characterToAdd)
        }
        
        return stringWithAddedSpaces
    }
}

Adapting this to other situations - like your delegate not being a ViewController - is left as an exercise for the reader.

Solution 3 - Ios

You can probably optimize my code or there might be an easier way but this code should work:

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

    __block NSString *text = [textField text];
    
    NSCharacterSet *characterSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789\b"];
    string = [string stringByReplacingOccurrencesOfString:@" " withString:@""];
    if ([string rangeOfCharacterFromSet:[characterSet invertedSet]].location != NSNotFound) {
        return NO;
    }

    text = [text stringByReplacingCharactersInRange:range withString:string];
    text = [text stringByReplacingOccurrencesOfString:@" " withString:@""];
    
    NSString *newString = @"";
    while (text.length > 0) {
        NSString *subString = [text substringToIndex:MIN(text.length, 4)];
        newString = [newString stringByAppendingString:subString];
        if (subString.length == 4) {
            newString = [newString stringByAppendingString:@" "];
        }
        text = [text substringFromIndex:MIN(text.length, 4)];
    }
    
    newString = [newString stringByTrimmingCharactersInSet:[characterSet invertedSet]];
    
    if (newString.length >= 20) {
        return NO;
    }
    
    [textField setText:newString];
    
    return NO;
}

Solution 4 - Ios

Swift 3 solution using Fawkes answer as basic. Added Amex Card format support. Added reformation when card type changed.

First make new class with this code:

extension String {
    
    func containsOnlyDigits() -> Bool
    {

        let notDigits = NSCharacterSet.decimalDigits.inverted

        if rangeOfCharacter(from: notDigits, options: String.CompareOptions.literal, range: nil) == nil
        {
            return true
        }
        
        return false
    }
}
import UIKit

var creditCardFormatter : CreditCardFormatter
{
    return CreditCardFormatter.sharedInstance
}

class CreditCardFormatter : NSObject
{
    static let sharedInstance : CreditCardFormatter = CreditCardFormatter()
    
    func formatToCreditCardNumber(isAmex: Bool, textField : UITextField, withPreviousTextContent previousTextContent : String?, andPreviousCursorPosition previousCursorSelection : UITextRange?) {
        var selectedRangeStart = textField.endOfDocument
        if textField.selectedTextRange?.start != nil {
            selectedRangeStart = (textField.selectedTextRange?.start)!
        }
        if  let textFieldText = textField.text
        {
            var targetCursorPosition : UInt = UInt(textField.offset(from:textField.beginningOfDocument, to: selectedRangeStart))
            let cardNumberWithoutSpaces : String = removeNonDigitsFromString(string: textFieldText, andPreserveCursorPosition: &targetCursorPosition)
            if cardNumberWithoutSpaces.characters.count > 19
            {
                textField.text = previousTextContent
                textField.selectedTextRange = previousCursorSelection
                return
            }
            var cardNumberWithSpaces = ""
            if isAmex {
                cardNumberWithSpaces = insertSpacesInAmexFormat(string: cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)
            }
            else
            {
                cardNumberWithSpaces = insertSpacesIntoEvery4DigitsIntoString(string: cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)
            }
            textField.text = cardNumberWithSpaces
            if let finalCursorPosition = textField.position(from:textField.beginningOfDocument, offset: Int(targetCursorPosition))
            {
                textField.selectedTextRange = textField.textRange(from: finalCursorPosition, to: finalCursorPosition)
            }
        }
    }
    
    func removeNonDigitsFromString(string : String, andPreserveCursorPosition cursorPosition : inout UInt) -> String {
        var digitsOnlyString : String = ""
        for index in stride(from: 0, to: string.characters.count, by: 1)
        {
            let charToAdd : Character = Array(string.characters)[index]
            if isDigit(character: charToAdd)
            {
                digitsOnlyString.append(charToAdd)
            }
            else
            {
                if index < Int(cursorPosition)
                {
                    cursorPosition -= 1
                }
            }
        }
        return digitsOnlyString
    }
    
    private func isDigit(character : Character) -> Bool
    {
        return "\(character)".containsOnlyDigits()
    }

    func insertSpacesInAmexFormat(string : String, andPreserveCursorPosition cursorPosition : inout UInt) -> String {
        var stringWithAddedSpaces : String = ""
        for index in stride(from: 0, to: string.characters.count, by: 1)
        {
            if index == 4
            {
                stringWithAddedSpaces += " "
                if index < Int(cursorPosition)
                {
                    cursorPosition += 1
                }
            }
            if index == 10 {
                stringWithAddedSpaces += " "
                if index < Int(cursorPosition)
                {
                    cursorPosition += 1
                }
            }
            if index < 15 {
               let characterToAdd : Character = Array(string.characters)[index]
                stringWithAddedSpaces.append(characterToAdd)
            }
        }
        return stringWithAddedSpaces
    }
    
    
    func insertSpacesIntoEvery4DigitsIntoString(string : String, andPreserveCursorPosition cursorPosition : inout UInt) -> String {
        var stringWithAddedSpaces : String = ""
        for index in stride(from: 0, to: string.characters.count, by: 1)
        {
            if index != 0 && index % 4 == 0 && index < 16
            {
                stringWithAddedSpaces += " "
                
                if index < Int(cursorPosition)
                {
                    cursorPosition += 1
                }
            }
            if index < 16 {
                let characterToAdd : Character = Array(string.characters)[index]
                stringWithAddedSpaces.append(characterToAdd)
            }
        }
        return stringWithAddedSpaces
    }
    
}

In your ViewControllerClass add this function

func reformatAsCardNumber(textField:UITextField){
  let formatter = CreditCardFormatter()
  var isAmex = false
  if selectedCardType == "AMEX" {
    isAmex = true
    }
  formatter.formatToCreditCardNumber(isAmex: isAmex, textField: textField, withPreviousTextContent: textField.text, andPreviousCursorPosition: textField.selectedTextRange)
}

Then add target to your textField

youtTextField.addTarget(self, action: #selector(self.reformatAsCardNumber(textField:)), for: UIControlEvents.editingChanged)

Register new variable and sent card type to it

var selectedCardType: String? {
  didSet{
    reformatAsCardNumber(textField: yourTextField)
  }
}

Thanks Fawkes for his code!

Solution 5 - Ios

I think this one is good:

-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
    {
        
        NSLog(@"%@",NSStringFromRange(range));
    
        // Only the 16 digits + 3 spaces
        if (range.location == 19) {
            return NO;
        }
        
        // Backspace
        if ([string length] == 0)
            return YES;
        
        if ((range.location == 4) || (range.location == 9) || (range.location == 14))
        {
            
            NSString *str    = [NSString stringWithFormat:@"%@ ",textField.text];
            textField.text   = str;
        }
    
        return YES;
    }

Solution 6 - Ios

So I wanted to this with less code, so I used the code here and repurposed it a little bit. I had two fields in the screen, one for the number and one for the expiry date, so I made it more reusable.

Swift 3 alternate answer

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
	guard let currentText = (textField.text as NSString?)?.replacingCharacters(in: range, with: string) else { return true }

	if textField == cardNumberTextField {
		textField.text = currentText.grouping(every: 4, with: " ")
		return false
	}
	else { // Expiry Date Text Field
		textField.text = currentText.grouping(every: 2, with: "/")
		return false
	}
}

extension String {
    func grouping(every groupSize: String.IndexDistance, with separator: Character) -> String {
	   let cleanedUpCopy = replacingOccurrences(of: String(separator), with: "")
	   return String(cleanedUpCopy.characters.enumerated().map() {
			$0.offset % groupSize == 0 ? [separator, $0.element] : [$0.element]
	   }.joined().dropFirst())
    }
}

Solution 7 - Ios

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
    {
        if textField == CardNumTxt
        {
            let replacementStringIsLegal = string.rangeOfCharacterFromSet(NSCharacterSet(charactersInString: "0123456789").invertedSet) == nil
        
            if !replacementStringIsLegal
            {
                return false
            }

            let newString = (textField.text! as NSString).stringByReplacingCharactersInRange(range, withString: string)
            let components = newString.componentsSeparatedByCharactersInSet(NSCharacterSet(charactersInString: "0123456789").invertedSet)
            
            let decimalString = components.joinWithSeparator("") as NSString
            let length = decimalString.length
            let hasLeadingOne = length > 0 && decimalString.characterAtIndex(0) == (1 as unichar)
            
            if length == 0 || (length > 16 && !hasLeadingOne) || length > 19
            {
                let newLength = (textField.text! as NSString).length + (string as NSString).length - range.length as Int
                
                return (newLength > 16) ? false : true
            }
            var index = 0 as Int
            let formattedString = NSMutableString()
            
            if hasLeadingOne
            {
                formattedString.appendString("1 ")
                index += 1
            }
            if length - index > 4
            {
                let prefix = decimalString.substringWithRange(NSMakeRange(index, 4))
                formattedString.appendFormat("%@-", prefix)
                index += 4
            }

            if length - index > 4
            {
                let prefix = decimalString.substringWithRange(NSMakeRange(index, 4))
                formattedString.appendFormat("%@-", prefix)
                index += 4
            }
            if length - index > 4
            {
                let prefix = decimalString.substringWithRange(NSMakeRange(index, 4))
                formattedString.appendFormat("%@-", prefix)
                index += 4
            }

            
            let remainder = decimalString.substringFromIndex(index)
            formattedString.appendString(remainder)
            textField.text = formattedString as String
            return false
        }
        else
        {
            return true
        }
    }

formattedString.appendFormat("%@-", prefix) chage of "-" any other your choose

Solution 8 - Ios

In Swift 5 :

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        if textField == cardNumberTextField {
            return formatCardNumber(textField: textField, shouldChangeCharactersInRange: range, replacementString: string)
        }
        return true
    }

    
    func formatCardNumber(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
        if textField == cardNumberTextField {
            let replacementStringIsLegal = string.rangeOfCharacter(from: NSCharacterSet(charactersIn: "0123456789").inverted) == nil

            if !replacementStringIsLegal {
                return false
            }

            let newString = (textField.text! as NSString).replacingCharacters(in: range, with: string)
            let components = newString.components(separatedBy: NSCharacterSet(charactersIn: "0123456789").inverted)
            let decimalString = components.joined(separator: "") as NSString
            let length = decimalString.length
            let hasLeadingOne = length > 0 && decimalString.character(at: 0) == (1 as unichar)

            if length == 0 || (length > 16 && !hasLeadingOne) || length > 19 {
                let newLength = (textField.text! as NSString).length + (string as NSString).length - range.length as Int

                return (newLength > 16) ? false : true
            }
            var index = 0 as Int
            let formattedString = NSMutableString()

            if hasLeadingOne {
                formattedString.append("1 ")
                index += 1
            }
            if length - index > 4 {
                let prefix = decimalString.substring(with: NSRange(location: index, length: 4))
                formattedString.appendFormat("%@ ", prefix)
                index += 4
            }

            if length - index > 4 {
                let prefix = decimalString.substring(with: NSRange(location: index, length: 4))
                formattedString.appendFormat("%@ ", prefix)
                index += 4
            }
            if length - index > 4 {
                let prefix = decimalString.substring(with: NSRange(location: index, length: 4))
                formattedString.appendFormat("%@ ", prefix)
                index += 4
            }

            let remainder = decimalString.substring(from: index)
            formattedString.append(remainder)
            textField.text = formattedString as String
            return false
        } else {
            return true
        }
    }

Solution 9 - Ios

Yet another version of the accepted answer in Swift 2...

Ensure you have these in your delegate instance:

private var previousTextFieldContent: String?
private var previousSelection: UITextRange?

And also ensure that your text field calls reformatAsCardNumber:

textField.addTarget(self, action: #selector(reformatAsCardNumber(_:)), forControlEvents: .EditingChanged)

You text field delegate will need to do this:

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    previousTextFieldContent = textField.text;
    previousSelection = textField.selectedTextRange;
    return true
}

Lastly include the following methods:

func reformatAsCardNumber(textField: UITextField) {
    var targetCursorPosition = 0
    if let startPosition = textField.selectedTextRange?.start {
        targetCursorPosition = textField.offsetFromPosition(textField.beginningOfDocument, toPosition: startPosition)
    }

    var cardNumberWithoutSpaces = ""
    if let text = textField.text {
        cardNumberWithoutSpaces = self.removeNonDigits(text, andPreserveCursorPosition: &targetCursorPosition)
    }

    if cardNumberWithoutSpaces.characters.count > 19 {
        textField.text = previousTextFieldContent
        textField.selectedTextRange = previousSelection
        return
    }

    let cardNumberWithSpaces = self.insertSpacesEveryFourDigitsIntoString(cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)
    textField.text = cardNumberWithSpaces
    
    if let targetPosition = textField.positionFromPosition(textField.beginningOfDocument, offset: targetCursorPosition) {
        textField.selectedTextRange = textField.textRangeFromPosition(targetPosition, toPosition: targetPosition)
    }
}

func removeNonDigits(string: String, inout andPreserveCursorPosition cursorPosition: Int) -> String {
    var digitsOnlyString = ""
    let originalCursorPosition = cursorPosition
    
    for i in 0.stride(to: string.characters.count, by: 1) {
        let characterToAdd = string[string.startIndex.advancedBy(i)]
        if characterToAdd >= "0" && characterToAdd <= "9" {
            digitsOnlyString.append(characterToAdd)
        }
        else if i < originalCursorPosition {
            cursorPosition -= 1
        }
    }
    
    return digitsOnlyString
}

func insertSpacesEveryFourDigitsIntoString(string: String, inout andPreserveCursorPosition cursorPosition: Int) -> String {
    var stringWithAddedSpaces = ""
    let cursorPositionInSpacelessString = cursorPosition
    
    for i in 0.stride(to: string.characters.count, by: 1) {
        if i > 0 && (i % 4) == 0 {
            stringWithAddedSpaces.appendContentsOf(" ")
            if i < cursorPositionInSpacelessString {
                cursorPosition += 1
            }
        }
        let characterToAdd = string[string.startIndex.advancedBy(i)]
        stringWithAddedSpaces.append(characterToAdd)
    }
    
    return stringWithAddedSpaces
}

Solution 10 - Ios

Here is the Swift 5 version of Mark Amery's accepted answer.

Add these variables in your class:

@IBOutlet weak var cardNumberTextField: UITextField!
private var previousTextFieldContent: String?
private var previousSelection: UITextRange?

Also ensure that your text field calls reformatAsCardNumber: from viewDidLoad():

cardNumberTextField.addTarget(self, action: #selector(reformatAsCardNumber), for: .editingChanged)

Add this in your UITextFieldDelegate:

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
   
    if textField == cardNumberTextField {
        previousTextFieldContent = textField.text;
        previousSelection = textField.selectedTextRange;
    }
    
    return true
}

Lastly include the following methods in your viewController:

@objc func reformatAsCardNumber(textField: UITextField) {
    var targetCursorPosition = 0
    if let startPosition = textField.selectedTextRange?.start {
        targetCursorPosition = textField.offset(from: textField.beginningOfDocument, to: startPosition)
    }
    
    var cardNumberWithoutSpaces = ""
    if let text = textField.text {
        cardNumberWithoutSpaces = self.removeNonDigits(string: text, andPreserveCursorPosition: &targetCursorPosition)
    }
    
    if cardNumberWithoutSpaces.count > 19 {
        textField.text = previousTextFieldContent
        textField.selectedTextRange = previousSelection
        return
    }
    
    let cardNumberWithSpaces = self.insertSpacesEveryFourDigitsIntoString(string: cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)
    textField.text = cardNumberWithSpaces
    
    if let targetPosition = textField.position(from: textField.beginningOfDocument, offset: targetCursorPosition) {
        textField.selectedTextRange = textField.textRange(from: targetPosition, to: targetPosition)
    }
}

func removeNonDigits(string: String, andPreserveCursorPosition cursorPosition: inout Int) -> String {
    var digitsOnlyString = ""
    let originalCursorPosition = cursorPosition
    
    for i in Swift.stride(from: 0, to: string.count, by: 1) {
        let characterToAdd = string[string.index(string.startIndex, offsetBy: i)]
        if characterToAdd >= "0" && characterToAdd <= "9" {
            digitsOnlyString.append(characterToAdd)
        }
        else if i < originalCursorPosition {
            cursorPosition -= 1
        }
    }
    
    return digitsOnlyString
}

func insertSpacesEveryFourDigitsIntoString(string: String, andPreserveCursorPosition cursorPosition: inout Int) -> String {
    var stringWithAddedSpaces = ""
    let cursorPositionInSpacelessString = cursorPosition
    
    for i in Swift.stride(from: 0, to: string.count, by: 1) {
        if i > 0 && (i % 4) == 0 {
            stringWithAddedSpaces.append(contentsOf: " ")
            if i < cursorPositionInSpacelessString {
                cursorPosition += 1
            }
        }
        let characterToAdd = string[string.index(string.startIndex, offsetBy: i)]
        stringWithAddedSpaces.append(characterToAdd)
    }
    
    return stringWithAddedSpaces
}

Solution 11 - Ios

Here is a Swift version in case this is useful to anyone still looking for this answer but using Swift instead of Objective-C. The concepts are still the same regardless.

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
{
    //range.length will be greater than 0 if user is deleting text - allow it to replace
    if range.length > 0
    {
        return true
    }
    
    //Don't allow empty strings
    if string == " "
    {
        return false
    }
    
    //Check for max length including the spacers we added
    if range.location == 20
    {
        return false
    }
    
    var originalText = textField.text
    let replacementText = string.stringByReplacingOccurrencesOfString(" ", withString: "")
    
    //Verify entered text is a numeric value
    let digits = NSCharacterSet.decimalDigitCharacterSet()
    for char in replacementText.unicodeScalars
    {
        if !digits.longCharacterIsMember(char.value)
        {
            return false
        }
    }
    
    //Put an empty space after every 4 places
    if originalText!.length() % 5 == 0
    {
        originalText?.appendContentsOf(" ")
        textField.text = originalText
    }
    
    return true
}

Solution 12 - Ios

In order to achieve the goal of format the text entered in the textfield in this way XXXX XXXX XXXX XXXX is important to keep in mind some important things. Beside the fact that the 16 digits card number separated every four digit is the most common used format, there are cards with 15 digits (AmEx formatted XXXX XXXXXX XXXXX) and others with 13 digits or even with 19 digits (https://en.wikipedia.org/wiki/Payment_card_number ). Other important thing you should consider is configure the textField to allow only digits, configure the keyboard type as numberPad is a good start, but is convenient to implement a method which secure the input.

A starting point is decide when you want to format the number, while the user is entering the number or when the user leave the text field. In the case that you want to format when the user leave the textField is convenient to use the textFieldDidEndEditing(_:) delegate's method take the content of the textField and format it.

In the case you while the user is entering the number is useful the textField(_:shouldChangeCharactersIn:replacementString:) delegate method which is called whenever the current text changes.

In both cases there is still a problem, figure out which is the correct format for the entered number, IMHO and based on all the numbers that I have seen, there are only two main formats: the Amex format with 15 digits described above and the format which group card number every four digits which don not care of how much digits there are, being this case like a generic rule, for example a card with 13 digits will be formatted XXXXX XXXX XXXX X and with 19 digits will look like this XXXX XXXX XXXX XXXX XXX, this will work for the most common cases (16 digits) and for the others as well. So you could figure out how to manage the AmEx case with the same algorithm below playing with the magic numbers.

I used a RegEx to ensure that a 15 digits card is an American express, in the case of other particular formats

let regex = NSPredicate(format: "SELF MATCHES %@", "3[47][A-Za-z0-9*-]{13,}" )
let isAmex = regex.evaluate(with: stringToValidate)

I strongly recommend to use the specific RegEx which is useful to identify the Issuer and to figure out how many digits should be accepted.

Now my swift approach of the solution with textFieldDidEndEditing is

func textFieldDidEndEditing(_ textField: UITextField) {
    
    _=format(cardNumber: textField.text!)
    
}
func format(cardNumber:String)->String{
    var formatedCardNumber = ""
    var i :Int = 0
    //loop for every character
    for character in cardNumber.characters{
        //in case you want to replace some digits in the middle with * for security
        if(i < 6 || i >= cardNumber.characters.count - 4){
            formatedCardNumber = formatedCardNumber + String(character)
        }else{
            formatedCardNumber = formatedCardNumber + "*"
        }
        //insert separators every 4 spaces(magic number)
        if(i == 3 || i == 7 || i == 11 || (i == 15 && cardNumber.characters.count > 16 )){
            formatedCardNumber = formatedCardNumber + "-"
            // could use just " " for spaces
        }
        
        i = i + 1
    }
    return formatedCardNumber
}

and for shouldChangeCharactersIn:replacementString: a Swift 3.0 From Jayesh Miruliya Answer, put a separator between the group of four characters

 func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
    {
        if textField == CardNumTxt
        {
            let replacementStringIsLegal = string.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789").inverted) == nil

        if !replacementStringIsLegal
        {
            return false
        }

        let newString = (textField.text! as NSString).replacingCharacters(in: range, with: string)
        let components = newString.components(separatedBy: CharacterSet(charactersIn: "0123456789").inverted)

        let decimalString = components.joined(separator: "") as NSString
        let length = decimalString.length
        let hasLeadingOne = length > 0 && decimalString.character(at: 0) == (1 as unichar)

        if length == 0 || (length > 16 && !hasLeadingOne) || length > 19
        {
            let newLength = (textField.text! as NSString).length + (string as NSString).length - range.length as Int

            return (newLength > 16) ? false : true
        }
        var index = 0 as Int
        let formattedString = NSMutableString()

        if hasLeadingOne
        {
            formattedString.append("1 ")
            index += 1
        }
        if length - index > 4 //magic number separata every four characters
        {
            let prefix = decimalString.substring(with: NSMakeRange(index, 4))
            formattedString.appendFormat("%@-", prefix)
            index += 4
        }

        if length - index > 4
        {
            let prefix = decimalString.substring(with: NSMakeRange(index, 4))
            formattedString.appendFormat("%@-", prefix)
            index += 4
        }
        if length - index > 4
        {
            let prefix = decimalString.substring(with: NSMakeRange(index, 4))
            formattedString.appendFormat("%@-", prefix)
            index += 4
        }


        let remainder = decimalString.substring(from: index)
        formattedString.append(remainder)
        textField.text = formattedString as String
        return false
        }
        else
        {
            return true
        }
    }

Solution 13 - Ios

Swift 3.2

Little correction in the @Lucas answer and working code in swift 3.2. Also removing the space character automatically.

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    
    if range.location == 19 {
        return false
    }
    
    if range.length == 1 {
        if (range.location == 5 || range.location == 10 || range.location == 15) {
            let text = textField.text ?? ""
            textField.text = text.substring(to: text.index(before: text.endIndex))
        }
        return true
    }
        
    if (range.location == 4 || range.location == 9 || range.location == 14) {
        textField.text = String(format: "%@ ", textField.text ?? "")
    }

    return true
}

Solution 14 - Ios

Define below method & call it in UITextfield delegates or wherever required

-(NSString*)processString :(NSString*)yourString
{
    if(yourString == nil){
        return @"";
    }
    int stringLength = (int)[yourString length];
    int len = 4;  // Length after which you need to place added character
    NSMutableString *str = [NSMutableString string];
    int i = 0;
    for (; i < stringLength; i+=len) {
        NSRange range = NSMakeRange(i, len);
        [str appendString:[yourString substringWithRange:range]];
        if(i!=stringLength -4){
            [str appendString:@" "]; //If required string format is XXXX-XXXX-XXXX-XXX then just replace [str appendString:@"-"]
        }
    }
    if (i < [str length]-1) {  // add remaining part
        [str appendString:[yourString substringFromIndex:i]];
    }
    //Returning required string
    
    return str;
}

Solution 15 - Ios

Swift 3 solution based upon Mark Amery's Objective-C solution:

  1. Implement action and delegate methods:

     textField.addTarget(self, action: #selector(reformatAsCardNumber(_:))
     textField.delegate = self
    
  2. TextField Delegate methods and other methods:

     func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
         previousTextFieldContent = textField.text;
         previousSelection = textField.selectedTextRange;
         return true
     }
     
     func reformatAsCardNumber(_ textField: UITextField) {
         var targetCursorPosition = 0
         if let startPosition = textField.selectedTextRange?.start {
             targetCursorPosition = textField.offset(from:textField.beginningOfDocument, to: startPosition)
         }
         
         var cardNumberWithoutSpaces = ""
         if let text = textField.text {
             cardNumberWithoutSpaces = removeNonDigits(string: text, andPreserveCursorPosition: &targetCursorPosition)
         }
         
         if cardNumberWithoutSpaces.characters.count > 19 {
             textField.text = previousTextFieldContent
             textField.selectedTextRange = previousSelection
             return
         }
         
         let cardNumberWithSpaces = self.insertSpacesEveryFourDigitsIntoString(string: cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)
         textField.text = cardNumberWithSpaces
         
         if let targetPosition = textField.position(from: textField.beginningOfDocument, offset: targetCursorPosition) {
             textField.selectedTextRange = textField.textRange(from: targetPosition, to: targetPosition)
         }
     }
     
     func removeNonDigits(string: String, andPreserveCursorPosition cursorPosition: inout Int) -> String {
         var digitsOnlyString = ""
         let originalCursorPosition = cursorPosition
         
         for i in stride(from: 0, to: string.characters.count, by: 1) {
             let characterToAdd =  string[string.index(string.startIndex, offsetBy: i)]
             if characterToAdd >= "0" && characterToAdd <= "9" {
                 digitsOnlyString.append(characterToAdd)
             }
             else if i < originalCursorPosition {
                 cursorPosition -= 1
             }
         }
         
         return digitsOnlyString
     }
     
     func insertSpacesEveryFourDigitsIntoString(string: String, andPreserveCursorPosition cursorPosition: inout Int) -> String {
         var stringWithAddedSpaces = ""
         let cursorPositionInSpacelessString = cursorPosition
         
         for i in stride(from: 0, to: string.characters.count, by: 1) {
             if i > 0 && (i % 4) == 0 {
                 stringWithAddedSpaces.append(" ")
                 if i < cursorPositionInSpacelessString {
                     cursorPosition += 1
                 }
             }
             let characterToAdd = string[string.index(string.startIndex, offsetBy: i)]
             stringWithAddedSpaces.append(characterToAdd)
         }
         
         return stringWithAddedSpaces
     }
    

Solution 16 - Ios

Swift 5.1, Xcode 11

After trying many solutions, I faced issues such as setting correct cursor position and formating as per need, I finally found a solution after combining 2 posts (https://stackoverflow.com/a/38838740/10579134, https://stackoverflow.com/a/45297778/10579134)

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    guard let currentText = (textField.text as NSString?)?.replacingCharacters(in: range, with: string) else { return true }
   
    
    if textField == yourTextField  {

        textField.setText(to: currentText.grouping(every: 4, with: "-"), preservingCursor: true)

        return false
    }
    return true
}

And adding this extension

extension UITextField {

public func setText(to newText: String, preservingCursor: Bool) {
    if preservingCursor {
        let cursorPosition = offset(from: beginningOfDocument, to: selectedTextRange!.start) + newText.count - (text?.count ?? 0)
        text = newText
        if let newPosition = self.position(from: beginningOfDocument, offset: cursorPosition) {
            selectedTextRange = textRange(from: newPosition, to: newPosition)
        }
    }
    else {
        text = newText
    }
}

Solution 17 - Ios

Here's a swift copy of the accepted answer. It is basically a wrapper class:

var creditCardFormatter : CreditCardFormatter
{
    return CreditCardFormatter.sharedInstance
}

class CreditCardFormatter : NSObject
{
    static let sharedInstance : CreditCardFormatter = CreditCardFormatter()
    
    func formatToCreditCardNumber(textField : UITextField, withPreviousTextContent previousTextContent : String?, andPreviousCursorPosition previousCursorSelection : UITextRange?)
    {
        if let selectedRangeStart = textField.selectedTextRange?.start, textFieldText = textField.text
        {
            var targetCursorPosition : UInt = UInt(textField.offsetFromPosition(textField.beginningOfDocument, toPosition: selectedRangeStart))
            
            let cardNumberWithoutSpaces : String = removeNonDigitsFromString(textFieldText, andPreserveCursorPosition: &targetCursorPosition)
            
            if cardNumberWithoutSpaces.characters.count > 19
            {
                textField.text = previousTextContent
                textField.selectedTextRange = previousCursorSelection
                return
            }
            
            let cardNumberWithSpaces : String = insertSpacesIntoEvery4DigitsIntoString(cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)
            
            textField.text = cardNumberWithSpaces
            
            if let finalCursorPosition = textField.positionFromPosition(textField.beginningOfDocument, offset: Int(targetCursorPosition))
            {
                textField.selectedTextRange = textField.textRangeFromPosition(finalCursorPosition, toPosition: finalCursorPosition)
            }
        }
    }
    
    func removeNonDigitsFromString(string : String,inout andPreserveCursorPosition cursorPosition : UInt) -> String
    {
        var digitsOnlyString : String = ""
        
        for index in 0.stride(to: string.characters.count, by: 1)
        {
            let charToAdd : Character = Array(string.characters)[index]
            
            if isDigit(charToAdd)
            {
                digitsOnlyString.append(charToAdd)
            }
            else
            {
                if index < Int(cursorPosition)
                {
                    cursorPosition -= 1
                }
            }
        }
        
        return digitsOnlyString
    }
    
    private func isDigit(character : Character) -> Bool
    {
        return "\(character)".containsOnlyDigits()
    }
    
    func insertSpacesIntoEvery4DigitsIntoString(string : String, inout andPreserveCursorPosition cursorPosition : UInt) -> String
    {
        var stringWithAddedSpaces : String = ""
        
        for index in 0.stride(to: string.characters.count, by: 1)
        {
            if index != 0 && index % 4 == 0
            {
                stringWithAddedSpaces += " "
                
                if index < Int(cursorPosition)
                {
                    cursorPosition += 1
                }
            }
            
            let characterToAdd : Character = Array(string.characters)[index]
            
            stringWithAddedSpaces.append(characterToAdd)
        }
        
        return stringWithAddedSpaces
    }

}

extension String
{
    func containsOnlyDigits() -> Bool
    {
        let notDigits : NSCharacterSet = NSCharacterSet.decimalDigitCharacterSet().invertedSet
        
        if (rangeOfCharacterFromSet(notDigits, options: NSStringCompareOptions.LiteralSearch, range: nil) == nil)
        {
            return true
        }
        
        return false
    }
}

Solution 18 - Ios

Here is a Kotlin answer based off of Mark Amery:

fun formatCardNumber(cardNumber: String): String {
    var trimmedCardNumber = cardNumber.replace(" ","")

    // UATP cards have 4-5-6 (XXXX-XXXXX-XXXXXX) format
    val is456 = trimmedCardNumber.startsWith("1")

    // These prefixes reliably indicate either a 4-6-5 or 4-6-4 card. We treat all these
    // as 4-6-5-4 to err on the side of always letting the user type more digits.
    val is465 = listOf("34", "37", "300", "301", "302", "303", "304", "305", "309", "36", "38", "39")
            .any { trimmedCardNumber.startsWith(it) }

    // In all other cases, assume 4-4-4-4.
    val is4444 = !(is456 || is465)

    trimmedCardNumber = if (is456 || is465) {
         trimmedCardNumber.take(cardNumberMaxLengthAmex)
    } else {
         trimmedCardNumber.take(cardNumberMaxLength)
    }

    var cardNumberWithAddedSpaces = ""

    trimmedCardNumber.forEachIndexed { index, c ->
        val needs465Spacing = is465 && (index == 4 || index == 10 || index == 15)
        val needs456Spacing = is456 && (index == 4 || index == 9 || index == 15)
        val needs4444Spacing = is4444 && index > 0 && index % 4 == 0

        if (needs465Spacing || needs456Spacing || needs4444Spacing) {
            cardNumberWithAddedSpaces += " "
        }

        cardNumberWithAddedSpaces += c
    }

    return cardNumberWithAddedSpaces
}

Then add a text changed listener on an edit text:

var flag = false

editText.addTextChangedListener(object : TextWatcher {
        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
            if (flag) { 
                flag = false 
            } else {
                val text = formatCardNumber(s.toString())
                flag = true
                editText.setText(text)
                editText.setSelection(text.count())
            }
        }

        override fun afterTextChanged(s: Editable?) {}
    })

Solution 19 - Ios

You can use my simple library: DECardNumberFormatter

Example:

// You can use it like default UITextField
let textField = DECardNumberTextField()
// Custom required setup
textField.setup()

Output:

For sample card number (Visa) 4111111111111111
Format (4-4-4-4): 4111 1111 1111 1111

For sample card number (AmEx) 341212345612345
Format (4-6-5): 3412 123456 12345

Solution 20 - Ios

Check Out This Solution. I found in Autorize.net SDK Example.

Make Your UITextField Keyboard Type to Numeric.

It Will Mask Credit Card Numbers With 'X' And By Adding Spaces It Will Make 'XXXX XXXX XXXX 1234' format.

> In Header .h file

    #define kSpace @" "
    #define kCreditCardLength 16
    #define kCreditCardLengthPlusSpaces (kCreditCardLength + 3)
    #define kCreditCardObscureLength (kCreditCardLength - 4)
    
    @property (nonatomic, strong) NSString *creditCardBuf;
    IBOutlet UITextField *txtCardNumber;

> In .m file

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
	if (textField == txtCardNumber) {
        if ([string length] > 0) { //NOT A BACK SPACE Add it
            
            if ([self isMaxLength:textField])
                return NO;
            
            self.creditCardBuf  = [NSString stringWithFormat:@"%@%@", self.creditCardBuf, string];
        } else {
            
            //Back Space do manual backspace
            if ([self.creditCardBuf length] > 1) {
                self.creditCardBuf = [self.creditCardBuf substringWithRange:NSMakeRange(0, [self.creditCardBuf length] - 1)];
            } else {
                self.creditCardBuf = @"";
            }
        }
        [self formatValue:textField];
    }
	
	return NO;
}

- (BOOL) isMaxLength:(UITextField *)textField {
    
    if (textField == txtCardNumber && [textField.text length] >= kCreditCardLengthPlusSpaces) {
        return YES;
    }
    return NO;
}

- (void) formatValue:(UITextField *)textField {
    NSMutableString *value = [NSMutableString string];
  
    if (textField == txtCardNumber) {
        NSInteger length = [self.creditCardBuf length];
        
        for (int i = 0; i < length; i++) {
            
            // Reveal only the last character.
            if (length <= kCreditCardObscureLength) {
                
                if (i == (length - 1)) {
                    [value appendString:[self.creditCardBuf substringWithRange:NSMakeRange(i,1)]];
                } else {
                    [value appendString:@“X”];
                }
            }
            // Reveal the last 4 characters
            else {
                
                if (i < kCreditCardObscureLength) {
                    [value appendString:@“X”];
                } else {
                    [value appendString:[self.creditCardBuf substringWithRange:NSMakeRange(i,1)]];
                }
            }
            
            //After 4 characters add a space
            if ((i +1) % 4 == 0 &&
                ([value length] < kCreditCardLengthPlusSpaces)) {
                [value appendString:kSpace];
            }
        }
        textField.text = value;
    }
}

Solution 21 - Ios

Here's a solution in Swift 2.2.1

extension UITextField {

	func setText(to newText: String, preservingCursor: Bool) {
		if preservingCursor {
			let cursorPosition = offsetFromPosition(beginningOfDocument, toPosition: selectedTextRange!.start) + newText.characters.count - (text?.characters.count ?? 0)
			text = newText
			if let newPosition = positionFromPosition(beginningOfDocument, offset: cursorPosition) {
				selectedTextRange = textRangeFromPosition(newPosition, toPosition: newPosition)
			}
		}
		else {
			text = newText
		}
	}
}

Now just put an IBAction in your view controller:

@IBAction func textFieldEditingChanged(sender: UITextField) {
    var digits = current.componentsSeparatedByCharactersInSet(NSCharacterSet.decimalDigitCharacterSet().invertedSet).joinWithSeparator("") // remove non-digits
    // add spaces as necessary or otherwise format your digits.
    // for example for a phone number or zip code or whatever
    // then just:
    sender.setText(to: digits, preservingCursor: true)
}

Solution 22 - Ios

Please use simple form of credite card /** See sample usage: ### let str = "41111111111111111"

 let x = yourClassname.setStringAsCardNumberWithSartNumber(4, withString: str!, withStrLenght: 8)
 
 ### output:- 4111XXXXXXXX1111
 
 let x = yourClassname.setStringAsCardNumberWithSartNumber(0, withString: str!, withStrLenght: 12)
 
 ### output: - XXXXXXXXXXXX1111
 
 */
func setStringAsCardNumberWithSartNumber(Number:Int,withString str:String ,withStrLenght len:Int ) -> String{
    //let aString: String = "41111111111111111"
    let arr = str.characters
    var CrediteCard : String = ""
    if arr.count > (Number + len) {
        for (index, element ) in arr.enumerate(){
            if index >= Number && index < (Number + len) {
                CrediteCard = CrediteCard + String("X")
            }else{
                CrediteCard = CrediteCard + String(element)
            }
        }
      return CrediteCard
    }else{
            print("\(Number) plus \(len) are grether than strings chatarter \(arr.count)")
    }
    print("\(CrediteCard)")
    return str
}

Solution 23 - Ios

Please check bellow solution, its working fine for me-

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
        
        let subString = (textField.text as! NSString).substringWithRange(range)
        if subString == " " && textField == cardNumberTextfield
        {
            return false     // user should not be able to delete space from card field
        }
        else if string == ""
        {
            return true      // user can delete any digit
        }
        
        
        // Expiry date formatting
        
        if textField == expiryDateTextfield
        {
            let str = textField.text! + string
            
            if str.length == 2 && Int(str) > 12
            {
                return false                  // Month should be <= 12
            }
            else if str.length == 2
            {
                textField.text = str+"/"      // append / after month
                return false
            }
            else if str.length > 5
            {
                return false                  // year should be in yy format
            }
        }
        
        
        
        // Card number formatting
        
        if textField == cardNumberTextfield
        {
            let str = textField.text! + string
            
            let stringWithoutSpace = str.stringByReplacingOccurrencesOfString(" ", withString: "")
           
            if stringWithoutSpace.length % 4 == 0 && (range.location == textField.text?.length)
            {
                if stringWithoutSpace.length != 16
                {
                    textField.text = str+" "    // add space after every 4 characters
                }
                else
                {
                    textField.text = str       // space should not be appended with last digit
                }
                
                return false
            }
            else if str.length > 19
            {
                return false
            }
        }

        
        
        return true
    }

Solution 24 - Ios

i modified @ilesh answer so it only shows the last 4 digits no matter what the lenght is. Also to ignore the space and "-" chars. This way, if we have a number with the format 0000 - 0000 - 0000 - 0000 it displays XXXX - XXXX - XXXX - 0000

func setStringAsCardNumberWithSartNumber(Number:Int,withString str:String) -> String{
    let arr = str.characters
    var CrediteCard : String = ""
    let len = str.characters.count-4
    if arr.count > (Number + len) {
        for (index, element ) in arr.enumerated(){
            if index >= Number && index < (Number + len) && element != "-" && element != " " {
                CrediteCard = CrediteCard + String("X")
            }else{
                CrediteCard = CrediteCard + String(element)
            }
        }
        return CrediteCard
    }else{
        print("\(Number) plus \(len) are grether than strings chatarter \(arr.count)")
    }
    print("\(CrediteCard)")
    return str
}

Solution 25 - Ios

Here is the modification of the answer from @sleeping_giant for swift. This solution formats the text in xxxx-xxxx-xxxx-xxxx-xxxx format and stops accepting any numbers beyond that range:

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
{
    if string == ""{
        return true
    }
    
    //range.length will be greater than 0 if user is deleting text - allow it to replace
    if range.length > 0
    {
        return true
    }
    
    //Don't allow empty strings
    if string == "-"
    {
        return false
    }
    
    //Check for max length including the spacers we added
    print(range.location)
    if range.location > 23
    {
        return false
    }
    
    var originalText = textField.text
    let replacementText = string.replacingOccurrences(of: "-", with: "")
    
    //Verify entered text is a numeric value
    let digits = NSCharacterSet.decimalDigits
    for char in replacementText.unicodeScalars
    {
        if !(digits as NSCharacterSet).longCharacterIsMember(char.value)
        {
            return false
        }
    }
    
    //Put an empty space after every 4 places
    if (originalText?.characters.count)! > 0
    {
        if (originalText?.characters.count)! < 5 && (originalText?.characters.count)! % 4 == 0{
            originalText?.append("-")
        }else if(((originalText?.characters.count)! + 1) % 5 == 0){
            originalText?.append("-")
        }
        
    }
    
    textField.text = originalText
    
    return true
}

Solution 26 - Ios

Found a GIST in Github that does exactly what I need in Swift3 (https://gist.github.com/nunogoncalves/6a8b4b21f4f69e0fc050190df96a1e56)

Implemented by doing ->

if creditCardNumberTextView.text?.characters.first == "3" {
    let validator = Validator(cardType: .americanExpress, value:  self.creditCardNumberTextView.text!).test()
        
      if validator == true {
       
       } else {
           
       }
   }

Works wonderfully in the APP I'm working out which uses credit cards.

Solution 27 - Ios

In my case, we have to formatting IBAN number. I think, the below code block help you

Firstly, check the user enterted value is valid:

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

    if(textField == self.ibanTextField){
        
           BOOL shouldChange =  ([Help checkTextFieldForIBAN:[NSString stringWithFormat:@"%@%@",textField.text,string]]);
 }
}

Secondly, you can see iban formated method just like below. Our IBAN formatted begin 2 letter.

+(BOOL)checkTextFieldForIBAN:(NSString*)string{
    
    string = [string stringByReplacingOccurrencesOfString:@" " withString:@""];
    
    if ([string length] <= 26) {
        
        if ([string length] > 2) {
            
            if ([self isLetter:[string substringToIndex:2]]) {
                
                if ([self isInteger:[string substringFromIndex:2]])
                    return YES;
                else
                    return NO;
                
            }else {
                
                return NO;
            }
        }else{
            
            return [self isLetter:string];
        }
        
    }
    else {
        
        return NO;
    }
    
    return YES;
}

Solution 28 - Ios

Create new swift file and paste below code, change text field class to VSTextField

import UIKit

public enum TextFieldFormatting {
    case uuid
    case socialSecurityNumber
    case phoneNumber
    case custom
    case noFormatting
}

public class VSTextField: UITextField {
    
    /**
     Set a formatting pattern for a number and define a replacement string. For example: If formattingPattern would be "##-##-AB-##" and
     replacement string would be "#" and user input would be "123456", final string would look like "12-34-AB-56"
     */
    public func setFormatting(_ formattingPattern: String, replacementChar: Character) {
        self.formattingPattern = formattingPattern
        self.replacementChar = replacementChar
        self.formatting = .custom
    }
    
    /**
     A character which will be replaced in formattingPattern by a number
     */
    public var replacementChar: Character = "*"
    
    /**
     A character which will be replaced in formattingPattern by a number
     */
    public var secureTextReplacementChar: Character = "\u{25cf}"
    
    /**
     True if input number is hexadecimal eg. UUID
     */
    public var isHexadecimal: Bool {
        return formatting == .uuid
    }
    
    /**
     Max length of input string. You don't have to set this if you set formattingPattern.
     If 0 -> no limit.
     */
    public var maxLength = 0
    
    /**
     Type of predefined text formatting. (You don't have to set this. It's more a future feature)
     */
    public var formatting : TextFieldFormatting = .noFormatting {
        didSet {
            switch formatting {
                
            case .socialSecurityNumber:
                self.formattingPattern = "***-**-****"
                self.replacementChar = "*"
                
            case .phoneNumber:
                self.formattingPattern = "***-***-****"
                self.replacementChar = "*"
                
            case .uuid:
                self.formattingPattern = "********-****-****-****-************"
                self.replacementChar = "*"
                
            default:
                self.maxLength = 0
            }
        }
    }
    
    /**
     String with formatting pattern for the text field.
     */
    public var formattingPattern: String = "" {
        didSet {
            self.maxLength = formattingPattern.count
        }
    }
    
    /**
     Provides secure text entry but KEEPS formatting. All digits are replaced with the bullet character \u{25cf} .
     */
    public var formatedSecureTextEntry: Bool {
        set {
            _formatedSecureTextEntry = newValue
            super.isSecureTextEntry = false
        }
        
        get {
            return _formatedSecureTextEntry
        }
    }
    
    override public var text: String! {
        set {
            super.text = newValue
            textDidChange() // format string properly even when it's set programatically
        }
        
        get {
            if case .noFormatting = formatting {
                return super.text
            } else {
                // Because the UIControl target action is called before NSNotificaion (from which we fire our custom formatting), we need to
                // force update finalStringWithoutFormatting to get the latest text. Otherwise, the last character would be missing.
                textDidChange()
                return finalStringWithoutFormatting
            }
        }
    }
    
    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        registerForNotifications()
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        registerForNotifications()
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
    
    /**
     Final text without formatting characters (read-only)
     */
    public var finalStringWithoutFormatting : String {
        return _textWithoutSecureBullets.keepOnlyDigits(isHexadecimal: isHexadecimal)
    }
    
    // MARK: - INTERNAL
    fileprivate var _formatedSecureTextEntry = false
    
    // if secureTextEntry is false, this value is similar to self.text
    // if secureTextEntry is true, you can find final formatted text without bullets here
    fileprivate var _textWithoutSecureBullets = ""
    
    fileprivate func registerForNotifications() {
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(VSTextField.textDidChange),
                                               name: NSNotification.Name(rawValue: "UITextFieldTextDidChangeNotification"),
                                               object: self)
    }
    
    @objc public func textDidChange() {
        var superText: String { return super.text ?? "" }
        
        // TODO: - Isn't there more elegant way how to do this?
        let currentTextForFormatting: String
        
        if superText.count > _textWithoutSecureBullets.count {
            currentTextForFormatting = _textWithoutSecureBullets + superText[superText.index(superText.startIndex, offsetBy: _textWithoutSecureBullets.count)...]
        } else if superText.count == 0 {
            _textWithoutSecureBullets = ""
            currentTextForFormatting = ""
        } else {
            currentTextForFormatting = String(_textWithoutSecureBullets[..<_textWithoutSecureBullets.index(_textWithoutSecureBullets.startIndex, offsetBy: superText.count)])
        }
        
        if formatting != .noFormatting && currentTextForFormatting.count > 0 && formattingPattern.count > 0 {
            let tempString = currentTextForFormatting.keepOnlyDigits(isHexadecimal: isHexadecimal)
            
            var finalText = ""
            var finalSecureText = ""
            
            var stop = false
            
            var formatterIndex = formattingPattern.startIndex
            var tempIndex = tempString.startIndex
            
            while !stop {
                let formattingPatternRange = formatterIndex ..< formattingPattern.index(formatterIndex, offsetBy: 1)
                if formattingPattern[formattingPatternRange] != String(replacementChar) {
                
                    finalText = finalText + formattingPattern[formattingPatternRange]
                    finalSecureText = finalSecureText + formattingPattern[formattingPatternRange]
                    
                } else if tempString.count > 0 {
                    
                    let pureStringRange = tempIndex ..< tempString.index(tempIndex, offsetBy: 1)
                    
                    finalText = finalText + tempString[pureStringRange]
                    
                    // we want the last number to be visible
                    if tempString.index(tempIndex, offsetBy: 1) == tempString.endIndex {
                        finalSecureText = finalSecureText + tempString[pureStringRange]
                    } else {
                        finalSecureText = finalSecureText + String(secureTextReplacementChar)
                    }
                    
                    tempIndex = tempString.index(after: tempIndex)
                }
                
                formatterIndex = formattingPattern.index(after: formatterIndex)
                
                if formatterIndex >= formattingPattern.endIndex || tempIndex >= tempString.endIndex {
                    stop = true
                }
            }
            
            _textWithoutSecureBullets = finalText
            
            let newText = _formatedSecureTextEntry ? finalSecureText : finalText
            if newText != superText {
                super.text = _formatedSecureTextEntry ? finalSecureText : finalText
            }
        }
        
        // Let's check if we have additional max length restrictions
        if maxLength > 0 {
            if superText.count > maxLength {
                super.text = String(superText[..<superText.index(superText.startIndex, offsetBy: maxLength)])
                _textWithoutSecureBullets = String(_textWithoutSecureBullets[..<_textWithoutSecureBullets.index(_textWithoutSecureBullets.startIndex, offsetBy: maxLength)])
            }
        }
    }
}


extension String {
    
    func keepOnlyDigits(isHexadecimal: Bool) -> String {
        let ucString = self.uppercased()
        let validCharacters = isHexadecimal ? "0123456789ABCDEF" : "0123456789"
        let characterSet: CharacterSet = CharacterSet(charactersIn: validCharacters)
        let stringArray = ucString.components(separatedBy: characterSet.inverted)
        let allNumbers = stringArray.joined(separator: "")
        return allNumbers
    }
}


// Helpers
fileprivate func < <T: Comparable>(lhs: T?, rhs: T?) -> Bool {
    switch (lhs, rhs) {
    case let (l?, r?):
        return l < r
    case (nil, _?):
        return true
    default:
        return false
    }
}

fileprivate func > <T: Comparable>(lhs: T?, rhs: T?) -> Bool {
    switch (lhs, rhs) {
    case let (l?, r?):
        return l > r
    default:
        return rhs < lhs
    }
}

More uses will be found on below links:

Solution 29 - Ios

You can use StringPatternFormatter pod:

pod 'StringPatternFormatter'

The UITextField need to implement the following method from UITextFieldDelegate:

import StringPatternFormatter

...

textField.delegate = self

...

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    guard let text = textField.text else {
        return true
    }
    
    let lastText = (text as NSString).replacingCharacters(in: range, with: string) as String
        
    textField.text = lastText.format("nnnn nnnn nnnn nnnn", oldString: text)
    return false
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionCan AksoyView Question on Stackoverflow
Solution 1 - IosMark AmeryView Answer on Stackoverflow
Solution 2 - IosMark AmeryView Answer on Stackoverflow
Solution 3 - IosSebrassiView Answer on Stackoverflow
Solution 4 - IosDmitryView Answer on Stackoverflow
Solution 5 - IosLucasView Answer on Stackoverflow
Solution 6 - IosDarvish KamaliaView Answer on Stackoverflow
Solution 7 - IosJayesh MiruliyaView Answer on Stackoverflow
Solution 8 - IosMaxView Answer on Stackoverflow
Solution 9 - IosLogicopolisView Answer on Stackoverflow
Solution 10 - IosFaris MuhammedView Answer on Stackoverflow
Solution 11 - IosSleeping_GiantView Answer on Stackoverflow
Solution 12 - IosAndrés SánchezView Answer on Stackoverflow
Solution 13 - Iosumair151View Answer on Stackoverflow
Solution 14 - IosYogesh LolusareView Answer on Stackoverflow
Solution 15 - IosmazoratiView Answer on Stackoverflow
Solution 16 - IosKedar SukerkarView Answer on Stackoverflow
Solution 17 - IosFawkesView Answer on Stackoverflow
Solution 18 - IosBlakeView Answer on Stackoverflow
Solution 19 - IosDarkngsView Answer on Stackoverflow
Solution 20 - IosNaeemView Answer on Stackoverflow
Solution 21 - IosDaniel T.View Answer on Stackoverflow
Solution 22 - IosIlesh PView Answer on Stackoverflow
Solution 23 - IosVishwas SinghView Answer on Stackoverflow
Solution 24 - IosFilipe SáView Answer on Stackoverflow
Solution 25 - IosDeepak BadigerView Answer on Stackoverflow
Solution 26 - IosSungView Answer on Stackoverflow
Solution 27 - IosEmre GürsesView Answer on Stackoverflow
Solution 28 - IosHaripal WaghView Answer on Stackoverflow
Solution 29 - IospableirosView Answer on Stackoverflow