格式化信用卡input的UITextField,如(xxxx xxxx xxxx xxxx)

我想格式化一个UITextFieldinput一个信用卡号码,只允许input数字,并自动插入空格,这样的数字格式如下:

 XXXX XXXX XXXX XXXX 

我怎样才能做到这一点?

首先,你确定要这样做吗? 虽然XXXX XXXX XXXX XXXX格式的信用卡和借记卡号码是最常见的,但它不是唯一的。 例如,美国运通卡有15位数字,通常以XXXX XXXXXX XXXXX格式写入,如下所示:

美国运通卡

即使Visa卡可以less于 16位,Maestro卡可以有更多:

18位数字的俄罗斯大师卡

您可能需要考虑您支持哪些卡networking(如Visa,Maestro等)以及您的应用将使用哪个或哪些国家/地区,并尝试了解这些国家/地区通常使用的卡号格式发卡机构。

你做到了,或者你已经确定你做的是正确的事情? 好的,那么…

简答题

只要做两件事。

首先,到你的UITextFieldDelegate ,添加这些实例variables…

 NSString *previousTextFieldContent; UITextRange *previousSelection; 

…和这些方法:

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

其次,设置reformatCardNumber:在文本字段触发UIControlEventEditingChanged事件时被调用:

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

(当然,在你的文本字段和委托被实例化之后,你需要做一些事情,如果你使用的是storyboard,view controller的viewDidLoad方法是一个合适的地方。

一些解释

这是一个看似复杂的问题。 两个重要的问题可能并不明显(以前的两个答案都没有考虑到):

  1. 用户与文本字段交互的方式多于在现有input末尾input单个字符。 您还必须正确处理用户在string中添加字符删除单个字符,删除多个选定字符以及粘贴多个字符。 对这个问题的一些更简单/更幼稚的方法将无法正确处理这些交互中的一些。 最不正当的情况是用户在string中间粘贴多个字符来replace其他字符,这个解决scheme通常足以处理这个问题。
  2. 用户修改后,您不仅需要正确地重新设置文本字段的文本格式,还需要合理地定位文本光标 。 天真地接近这个问题,不考虑这个问题几乎肯定会在某些情况下最终做文本光标愚蠢的事情(比如在用户在中间添加一个数字后把它放到文本字段的末尾)。

还有另一个陷阱潜伏在这里,这比上面两个重要得多,但是让人烦恼:

  • textField: shouldChangeCharactersInRange: replacementString返回NO textField: shouldChangeCharactersInRange: replacementString打破用户在文本字段中select文本时获得的“剪切”button,这就是为什么我不这样做。 从该方法返回NO导致“剪切”根本不更新剪贴板,我知道没有修复或解决方法。 因此,我们需要在UIControlEventEditingChanged处理程序中重新格式化文本字段,而不是(更明显)在shouldChangeCharactersInRange:本身中。

    幸运的是,在UI更新刷新到屏幕之前,UIControl事件处理程序似乎被调用,所以这种方法工作正常。

处理问题#1 (以及上面代码中使用的方法)的最简单和最简单的方法是,每当文本字段的内容发生变化时,将所有空格删除并重新插入到正确的位置,从而使我们无需什么样的文本操作(插入,删除或replace)正在进行,并以不同的方式处理这些可能性。

为了处理问题#2 ,我们跟踪光标的所需索引如何随着我们去掉非数字然后插入空格而发生变化。 这就是为什么代码使用NSMutableString逐字符执行这些操作的原因,而不是使用NSString的stringreplace方法。

还有一大堆关于文本字段应该如何performance的小问题,这些问题没有明显的正确答案:

  • 如果用户试图粘贴的东西,会导致文本字段的内容超过19位,如果粘贴的string的开始插入(直到19位数字到达),其余的裁剪,或者根本不应该插入?
  • 如果用户试图通过将光标定位在其后面并按下退格键来删除单个空间,则什么都不会发生,并且光标保持在原来的位置,如果光标左移一个字符(将其放置在空格之前),或者应该空格左侧的数字将被删除,就像光标已经离开空间了一样?
  • 当用户input第四,第八或第十二位数字时,是否应该立即插入一个空格,光标是否在其后面移动,或者在用户键入第五,第九或第十三位数字后才能插入空格?
  • 当用户删除空格后的第一个数字,如果这不会导致空间被完全删除,这应导致他们的光标位于空间之前或之后?

对于这些问题的答案可能是足够的,但是我只列出它们,只是为了清楚地表明,如果你有足够的迷恋,实际上有很多特殊情况你可能要仔细考虑。 在上面的代码中,我select了对我来说似乎合理的这些问题的答案。 如果您碰巧对这些与我的代码行为方式不兼容的点有强烈的感受,那么应该很容易就可以根据您的需求进行调整。

你可能可以优化我的代码,或者可能有一个更简单的方法,但这个代码应该工作:

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

我觉得这个很好:

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

Swift 3解决scheme使用Fawkes的答案为基础。 增加了Amex卡格式支持。 卡types改变时增加了改造。

首先用这个代码创build新的类:

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

在你的ViewControllerClass中添加这个函数

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

然后将目标添加到您的textField

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

注册新variables并发送卡types

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

感谢福克斯为他的代码!

 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(“%@ – ”,前缀)“ – ”的任何其他select的chage

这里是一个Swift版本,以防止任何人仍然在寻找这个答案,但使用Swift而不是Objective-C。 无论如何,这些概念仍然是一样的。

 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 } 

所以我想用更less的代码来做到这一点,所以我在这里使用了代码并重新使用了一下。 屏幕上有两个字段,一个是数字,另一个是截止date,所以我使它更加可重用。

Swift 3替代答案

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

定义下面的方法并在UITextfield代理或任何需要的地方调用它

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

为了达到以这种方式在文本框中input文本的目的,XXXX XXXX XXXX XXXX对于记住一些重要的事情很重要。 除了每4位数字分隔的16位数字卡号是最常用的格式外,还有15位数字的卡片(AmEx格式为XXXX XXXXXX XXXXX),其余的为13位数字,甚至19位数字( https:// en。 wikipedia.org/wiki/Payment_card_number )。 其他重要的事情,你应该考虑的是configurationtextField只允许数字,configuration键盘types为numberPad是一个好的开始,但方便实施一个方法,确保input。

起始点是决定何时格式化数字,用户input数字或用户离开文本字段时。 在你想要格式化的情况下,当用户离开textField时很方便的使用textFieldDidEndEditing(_ :)委托的方法来获取textField的内容并进行格式化。

在用户input数字的情况下,当前文本发生更改时调用textField(_:shouldChangeCharactersIn:replacementString :)委托方法。

在这两种情况下,仍然有一个问题,找出input的数字是正确的格式,恕我直言,并根据我所看到的所有数字,只有两种主要格式:上述15位数的Amex格式和格式是哪组卡号码每四位数字不在乎有多less数字,这种情况就像一个通用的规则,例如一个13位数字的卡片将被格式化XXXXX XXXX XXXX X和19位数字看起来像这样XXXX XXXX XXXX XXXX XXX,这将适用于最常见的情况(16位数字)以及其他情况。 所以你可以弄清楚如何使用相同的algorithm来pipe理AmEx的情况下,玩魔法数字。

在使用其他特定格式的情况下,我使用RegEx来确保15位数字卡是美国运通卡

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

我强烈build议使用特定的RegEx来识别发行者,并找出应该接受的数字。

现在我用textFieldDidEndEditing解决scheme的快速方法是

 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 } 

和forChangeCharactersIn:replacementString:一个Swift 3.0从Jayesh Miruliya回答,把一个分隔符的四个字符组之间

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

如果有人需要,这里有一个被接受的答案的快速复制。 它基本上是一个包装类。 我没有花太多的时间来优化它,但它已经可以使用了。

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

Yet another version of the accepted answer in Swift…

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 } 

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

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 } 

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 } 

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; 

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

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 } 

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 } 

I hope this is helpful to you.

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