Formateo de un UITextField para la entrada de la tarjeta de crédito como (xxxx xxxx xxxx xxxx)

Quiero formatear un UITextField para ingresar un número de tarjeta de crédito de forma tal que solo permita el ingreso de dígitos y automáticamente inserte espacios para que el número tenga el formato siguiente:

 XXXX XXXX XXXX XXXX 

¿Cómo puedo hacer esto?

Si está usando Swift, lea mi puerto de esta respuesta para Swift 4 y utilícelo en su lugar.

Si estás en Objective-C …

En primer lugar, a su UITextFieldDelegate , agregue estas variables de instancia …

 NSString *previousTextFieldContent; UITextRange *previousSelection; 

… y estos métodos:

 // 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; } 

En segundo lugar, configure reformatCardNumber: para que se llame siempre que el campo de texto active un evento UIControlEventEditingChanged :

 [yourTextField addTarget:yourTextFieldDelegate action:@selector(reformatAsCardNumber:) forControlEvents:UIControlEventEditingChanged]; 

(Por supuesto, tendrás que hacer esto en algún momento después de que tu campo de texto y su delegado hayan sido instanciados. Si estás usando storyboards, el método viewDidLoad de tu controlador de vista es un lugar apropiado.

Algunas explicaciones

Este es un problema engañosamente complicado. Tres cuestiones importantes que pueden no ser inmediatamente obvias (y las respuestas anteriores aquí no se tienen en cuenta):

  1. Si bien el formato XXXX XXXX XXXX XXXX para números de tarjetas de crédito y débito es el más común, no es el único. Por ejemplo, las tarjetas American Express tienen números de 15 dígitos generalmente escritos en formato XXXX XXXXXX XXXXX , como este:

    Una tarjeta American Express

    Incluso las tarjetas Visa pueden tener menos de 16 dígitos, y las tarjetas Maestro pueden tener más:

    Una tarjeta Maestro rusa con 18 dígitos

  2. Hay más maneras para que el usuario interactúe con un campo de texto que simplemente escribiendo caracteres individuales al final de su entrada existente. También debe controlar adecuadamente al usuario agregando caracteres en el medio de la cadena, eliminando caracteres individuales, eliminando múltiples caracteres seleccionados y pegando en varios caracteres. Algunos enfoques más simples / más ingenuos para este problema no lograrán manejar algunas de estas interacciones correctamente. El caso más perverso es que el usuario pegue en varios caracteres en el medio de la cadena para reemplazar otros caracteres, y esta solución es lo suficientemente general como para manejar eso.

  3. No solo necesita volver a formatear correctamente el texto del campo de texto después de que el usuario lo modifique; también debe colocar el cursor del texto con sensatez. Los enfoques ingenuos al problema que no tienen esto en cuenta terminarán haciendo algo bastante tonto con el cursor de texto en algunos casos (como ponerlo al final del campo de texto después de que el usuario agregue un dígito en el medio del mismo) )

Para tratar el problema n. ° 1, utilizamos el mapeo parcial de los prefijos del número de tarjeta a los formatos seleccionados por The Baymard Institute en https://baymard.com/checkout-usability/credit-card-patterns . Podemos detectar automáticamente el proveedor de la tarjeta de los primeros dos dígitos y (en algunos casos) deducir el formato y ajustar nuestro formato en consecuencia. Gracias a cnotethegr8 por contribuir con esta idea a esta respuesta.

La forma más simple y fácil de lidiar con el problema n . ° 2 (y el modo utilizado en el código anterior) es eliminar todos los espacios y reinsertarlos en las posiciones correctas cada vez que cambia el contenido del campo de texto, evitándonos la necesidad de calcular qué tipo de manipulación de texto (una inserción, una eliminación o un reemplazo) está sucediendo y maneje las posibilidades de manera diferente.

Para lidiar con el problema n . ° 3 , hacemos un seguimiento de cómo cambia el índice deseado del cursor a medida que eliminamos los dígitos que no son de dígitos y luego insertamos espacios. Esta es la razón por la que el código realiza estas manipulaciones de forma muy exhaustiva carácter por carácter utilizando NSMutableString , en lugar de utilizar los métodos de sustitución de cadenas de NSString .

Finalmente, hay una trampa más al acecho: devolver NO desde textField: shouldChangeCharactersInRange: replacementString rompe el botón 'Cortar' que el usuario obtiene cuando seleccionan texto en el campo de texto, por lo que yo no lo hago. Si devuelve NO de ese método, 'Cortar' simplemente no actualiza el portapapeles, y no conozco ninguna solución o solución. Como resultado, tenemos que hacer el reformateo del campo de texto en un controlador UIControlEventEditingChanged lugar de (más obviamente) en shouldChangeCharactersInRange: sí mismo.

Afortunadamente, los controladores de eventos de UIControl parecen ser llamados antes de que las actualizaciones de UI se vayan a la pantalla, por lo que este enfoque funciona bien.

También hay un montón de preguntas menores sobre cómo debe comportarse exactamente el campo de texto que no tiene respuestas obvias correctas:

  • Si el usuario intenta pegar algo que haría que el contenido del campo de texto exceda los 19 dígitos, debe insertarse el comienzo de la cadena pegada (hasta que se scopen 19 dígitos) y el rest recortar, o no debe insertarse nada. ?
  • Si el usuario intenta eliminar un espacio individual colocando su cursor detrás de él y presionando la tecla de retroceso, si no pasa nada y el cursor permanece donde está, si el cursor se mueve un caracter izquierdo (colocándolo antes del espacio), o si el Se borrará el dígito a la izquierda del espacio como si el cursor ya estuviera a la izquierda del espacio?
  • Cuando el usuario escribe en el cuarto, octavo o duodécimo dígito, ¿debe insertarse inmediatamente un espacio y el cursor moverse después de él, o debe insertarse solo después de que el usuario escriba el quinto, noveno o decimotercer dígito?
  • Cuando el usuario elimina el primer dígito después de un espacio, si esto no hace que el espacio se elimine por completo, ¿esto debería conducir a que su cursor se coloque antes o después del espacio?

Probablemente cualquier respuesta a cualquiera de estas preguntas sea adecuada, pero las incluyo solo para dejar en claro que en realidad hay muchos casos especiales que tal vez desee considerar detenidamente aquí, si fue lo suficientemente obsesivo. En el código anterior, seleccioné las respuestas a estas preguntas que me parecieron razonables. Si tiene sentimientos fuertes sobre alguno de estos puntos que no son compatibles con la forma en que se comporta mi código, debería ser lo suficientemente fácil ajustarlo a sus necesidades.

Probablemente pueda optimizar mi código o podría haber una manera más fácil, pero este código debería funcionar:

 -(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; } 

Creo que este es bueno:

 -(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; } 

La solución Swift 3 usando Fawkes responde como básica. Se agregó compatibilidad con formato de tarjeta Amex. Se agregó una reforma cuando el tipo de tarjeta cambió.

Primero crea una nueva clase con este código:

 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 } } 

En su ViewControllerClass agregue esta función

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

A continuación, agregue el objective a su campo de texto

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

Registre la nueva variable y el tipo de tarjeta enviado

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

Gracias Fawkes por su código!

 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 “-” cualquier otro que elijas

Sin embargo, otra versión de la respuesta aceptada en Swift 2 …

Asegúrese de tenerlos en su instancia de delegado:

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

Y también asegúrese de que su campo de texto llame a reformatAsCardNumber:

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

Tu delegado de campo de texto tendrá que hacer esto:

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

Por último, incluye los siguientes métodos:

 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 } 

A continuación se muestra un puerto Swift 4 de trabajo de la respuesta de Logicopolis (que a su vez es un puerto Swift 2 de una versión anterior de mi respuesta aceptada en Objective-C) mejorado con el truco de cnotethegr8 para admitir tarjetas Amex y luego mejorado para admitir más tarjetas formatos. Sugiero revisar la respuesta aceptada si aún no lo ha hecho, ya que ayuda a explicar la motivación detrás de gran parte de este código.

Tenga en cuenta que la serie mínima de pasos necesarios para ver esto en acción es:

  1. Crea una nueva aplicación de vista única en Swift.
  2. En Main.storyboard , agregue un campo de texto .
  3. Convierta ViewController el delegado del campo de texto .
  4. Pegue el siguiente código en ViewController.swift .
  5. Conecte el IBOutlet al campo de texto .
  6. Ejecute su aplicación y escriba en el campo de texto .

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

Adaptar esto a otras situaciones, como que tu delegado no sea un ViewController , se deja como un ejercicio para el lector.

Aquí hay una versión de Swift en caso de que sea útil para cualquiera que todavía esté buscando esta respuesta, pero usando Swift en lugar de Objective-C. Los conceptos siguen siendo los mismos independientemente.

 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 } 

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; } 

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 } } 

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()) } } 

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 } 

Here’s a swift copy of the accepted answer in case somebody needs it. It is basically a wrapper class. I haven’t spent too much time on optimising it, but it’s ready for use.

 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 } } 

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 } 

These answers are all just way too much code for me. 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) } 

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 } 

Espero que esto sea útil para ti.

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 } 

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.

in my case, we have to formating 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 formated 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; } 

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 } 

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; 

En archivo .m

 - (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; } } 

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 } 
 //: Playground - noun: a place where people can play import UIKit let cardNumber = "1234 8940 8941 5678" let aString = cardNumber.components(separatedBy:" ") var starString:String = "" for i in 0...(aString.count - 2) { starString = "\(starString) \(getStars(aString[i]))" } let finalCardString = starString + " " + aString.last! print(finalCardString) func getStars(_ digitString:String) -> String { let digits = digitString.flatMap{Int(String($0))} switch digits.count { case 1: return "*" case 2: return "**" case 3: return "***" case 4: return "****" case 5: return "*****" case 6: return "******" case 7: return "*******" case 8: return "********" case 9: return "*********" default: return "" } } 

//Output **** **** **** 5678

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 progtwigtically } 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[.. 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 < (lhs: T?, rhs: T?) -> Bool { switch (lhs, rhs) { case let (l?, r?): return l < r case (nil, _?): return true default: return false } } fileprivate func > (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 link

Thanks to the guy who provided the great solution for formatting text in UITextField.

http://vojtastavik.com/2015/03/29/real-time-formatting-in-uitextfield-swift-basics/

https://github.com/VojtaStavik/VSTextField

Working great for me.

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?) {} }) 

Prueba esto, funciona para mí

 func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if(textField == self.txtCardNumber) { 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 }