格式化信用卡输入的 UITextField,如(xxxx xxx xxx xxxx)

我想格式化一个 UITextField输入信用卡号码,使它只允许数字输入,并自动插入空格,使数字的格式如下:

XXXX XXXX XXXX XXXX

我怎么能这么做?

72813 次浏览

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

-(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,请阅读 我的港口这个答案为迅捷4并使用它。

如果你在 C 目标..。

首先,在 UITextFieldDelegate中添加这些实例变量..。

NSString *previousTextFieldContent;
UITextRange *previousSelection;

还有这些方法:

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

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

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

(当然,在文本字段及其委托被实例化之后,您将需要在某个时候执行此操作。如果您正在使用情节串连图板,那么视图控制器的 viewDidLoad方法是一个合适的位置。

一些解释

这是一个看似复杂的问题。有三个重要问题可能不会立即显现出来(以前的答案都没有考虑到这一点) :

  1. 虽然用于信用卡和借记卡号码的 XXXX XXXX XXXX XXXX格式是最常见的格式,但它不是唯一的格式。例如,美国运通卡有15位数字,通常以 XXXX XXXXXX XXXXX格式书写,如下所示:

    An American Express card

    甚至 Visa 卡也可以有 少于16位数字,Maestro 卡可以有更多:

    A Russian Maestro card with 18 digits

  2. 除了在现有输入的末尾键入单个字符之外,用户与文本字段交互的方式还有很多。您还必须正确处理字符串的用户 在中间添加字符删除单个字符、删除多个选定字符以及多个字符的 粘贴。解决这个问题的一些更简单/更天真的方法将无法正确处理其中的一些交互。最反常的情况是用户在字符串的中间粘贴多个字符以替换其他字符,这种解决方案非常通用,足以处理这种情况。

  3. 您不仅需要在用户修改后正确地重新格式化文本字段的文本-您还需要合理地定位 文本光标。天真的方法解决问题,如果不考虑这一点,几乎肯定会在某些情况下做一些愚蠢的文本光标(比如在用户在文本字段中间添加一个数字后将其放在文本字段的末尾)。

为了处理问题 # 1,我们使用卡号前缀到由 Baymard Institute 在 https://baymard.com/checkout-usability/credit-card-patterns策划的格式的部分映射。我们可以自动检测卡提供商从第一对数字和(在 一些的情况下)推断格式,并相应地调整我们的格式。感谢 (西班牙语)为这个答案贡献了这个想法。

处理问题 # 2(以及上面代码中使用的方法)的最简单和最容易的方法是,每次文本字段的内容发生变化时,去掉所有空格并将其重新插入到正确的位置,这样我们就不需要弄清楚正在进行哪种文本操作(插入、删除或替换) ,并以不同的方式处理各种可能性。

为了处理问题 # 3,当我们去掉非数字然后插入空格时,我们跟踪所需的光标索引是如何变化的。这就是为什么代码相当冗长地使用 NSMutableString逐字符执行这些操作,而不是使用 NSString的字符串替换方法。

最后,还有一个隐藏的陷阱: 从 textField: shouldChangeCharactersInRange: replacementString返回 NO会打破用户在文本字段中选择文本时获得的“ Cut”按钮,这就是为什么我不这样做。从那个方法返回 NO会导致‘ Cut’根本不更新剪贴板,而且我也不知道什么修复或变通方法。因此,我们需要在 UIControlEventEditingChanged处理程序中重新格式化文本字段,而不是(更明显地)在 shouldChangeCharactersInRange:本身中。

幸运的是,UIControl 事件处理程序似乎是在 UI 更新刷新到屏幕之前被调用的,因此这种方法可以很好地工作。

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

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

也许这些问题的任何一个答案都足够了,但是我列出它们只是为了说明实际上有很多特殊情况,如果你足够执着的话,你可能需要在这里仔细考虑一下。在上面的代码中,我选择了对我来说合理的问题的答案。如果您碰巧对这些不符合我的代码行为方式的点有强烈的感觉,那么应该很容易根据您的需要进行调整。

定义下面的方法并在 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;
}

我在 Autorize.net SDK示例中找到了这个解决方案。

将键盘类型设置为 Numeric

它将用“ X”来掩盖信用卡号码,并通过添加空格使 'XXXX XXXX XXXX 1234'格式化。

在 Header. h 文件中

    #define kSpace @" "
#define kCreditCardLength 16
#define kCreditCardLengthPlusSpaces (kCreditCardLength + 3)
#define kCreditCardObscureLength (kCreditCardLength - 4)


@property (nonatomic, strong) NSString *creditCardBuf;
IBOutlet UITextField *txtCardNumber;

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

这里有一个 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
}
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
}
}

其他格式(“%@-”,前缀)更改为“-”

下面是一个已被接受的答案的快速副本,它基本上是一个包装类:

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 2..。

确保在委托实例中包含以下内容:

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

并确保您的文本字段调用 reformAsCardNumber:

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

您的文本字段委托将需要执行以下操作:

func textField(textField: UITextField, shouldChangeCharactersInRange 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.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
}

以下是 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
}
}
}

现在只需在视图控制器中放入一个 IBAction:

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

请使用简单形式的信用卡 /** 参见示例用法: # # # let str = “4111111111111”

 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
}

请检查下面的溶液,它对我来说工作正常-

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
}

基于 Mark Amery 的 Objective-C 解决方案的 Swift 3解决方案:

  1. 实施行动和授权方法:

    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
    }
    

福克斯答案为基础的 Swift 3 解决方案。 增加了美国运通卡的格式支持。 增加了改造时卡类型变化。

首先使用以下代码创建新类:

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

然后将 target 添加到 textField

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

注册新变量并向其发送卡片类型

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

谢谢福克斯的代码!

我修改了@ilesh 的答案,所以它只显示最后4位数字,不管长度是多少。还要忽略空格和“-”字符。 这样,如果我们有一个格式为0000-0000-0000-0000的数字,它将显示 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
}

以下是“睡眠巨人”对“迅速”的修改答案。该解决方案将文本格式化为 xxxx-xxxx-xxxx-xxxx-xxxx格式,并停止接受超出该范围的任何数字:

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
}

在 Github 中找到了一个 GIST,它完全符合我在 Swift3(https://gist.github.com/nunogoncalves/6a8b4b21f4f69e0fc050190df96a1e56)中的需要

通过 doing-> 实现

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


if validator == true {


} else {


}
}

在我正在开发的使用信用卡的应用程序中非常有效。

在我的情况下,我们必须格式化 IBAN 号码。我认为,下面的代码块帮助你

首先,检查用户输入的值是否有效:

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


if(textField == self.ibanTextField){
        

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

其次,你可以看到如下所示的 IBAN 格式化方法。我们的 IBAN 格式化以2个字母开头。

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

为了达到格式化的目的,文本输入框中的文本就是这样输入的 重要的是要记住一些重要的事情。 除此之外,16位数字的卡号分隔每四个数字是最常用的 有15位数字的卡片(美国运通 XXXX XXXXXX XXXXX 格式)和其他13位数字或甚至19位数字的卡片(https://en.wikipedia.org/wiki/Payment_card_number ).您应该考虑的其他重要事情是将 textField 配置为只允许数字,将键盘类型配置为 numberPad 是一个很好的开始,但是可以方便地实现一个保护输入的方法。

起始点是决定何时需要格式化数字,而用户正在输入 数字或用户何时离开文本字段。 如果希望在用户离开 textField 时进行格式化,可以方便地 使用 textFieldDidEndEditing (_:)委托的方法获取 textField 的内容 然后格式化。

在这种情况下,当用户输入数字时,使用 TextField (_: should dChangePersontersIn: replace ementString:)委托方法,该方法调用 无论当前文本何时更改。

在这两种情况下仍然存在一个问题,找出输入数字的正确格式,恕我直言,基于我看到的所有数字,只有两种主要格式: 上述15位数字的 Amex 格式和每4位数字一组卡号的格式,这并不关心有多少位数字,在这种情况下就像一个通用规则,例如一张13位数字的卡片将被格式化为 XXXXX XXXX XXXX X,19位数字将看起来像这个 XXXX XXXX XXX XXX,这将工作在最常见的情况下(16位数字)和其他。因此,你可以想出如何管理美国运通的情况下,同样的算法下面发挥神奇的数字。

我使用了正则表达式,以确保15位数字卡是美国运通卡,在其他特定格式的情况下

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

我强烈建议使用特定的正则表达式,这对于识别发行者和计算应该接受多少位数字是有用的。

现在,我使用 textFieldDidEndEditing 的快速解决方案是

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
}

以及 should dChangePersontersIn: replace ementString: a Swift 3.0 From Jayesh Miruliya Answer,在四个字符组之间放置一个分隔符

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

所以我想用更少的代码,所以我使用代码 给你,并重新设置了一点。我在屏幕上有两个字段,一个表示数字,一个表示过期日期,所以我让它更具可重用性。

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

Swift 3.2

在@Lucas 的回答和工作代码中有一点小小的更正,在 Swift 3.2中也自动删除了空格字符。

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
}

下面是 Logicopolis 的答案的一个工作的 Swift 4端口(反过来是 Objective-C 中我的 接受的答案旧版本的 Swift 2端口) ,用 < a href = “ https://stackoverflow. com/users/278629/cnote thegr8”> cnote thegr8进行了增强 的技巧,支持美国运通卡,然后进一步增强,以支持更多的卡格式。如果您还没有接受这个答案,我建议您仔细阅读它,因为它有助于解释许多代码背后的动机。

请注意,要看到这种情况的实际发生,需要执行的最小步骤是:

  1. 在 Swift 中创建一个新的 单视图应用程序
  2. Main.storyboard上添加 文本字段
  3. 使 ViewController成为 文本字段的委托。
  4. 将下面的代码粘贴到 ViewController.swift中。
  5. IBOutlet连接到 文本字段
  6. 运行你的应用程序,输入 文本字段

import UIKit


class ViewController: UIViewController, UITextFieldDelegate {
private var previousTextFieldContent: String?
private var previousSelection: UITextRange?
@IBOutlet var yourTextField: UITextField!;


override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib
yourTextField.addTarget(self, action: #selector(reformatAsCardNumber), for: .editingChanged)
}


override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}


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


@objc func reformatAsCardNumber(textField: UITextField) {
var targetCursorPosition = 0
if let startPosition = textField.selectedTextRange?.start {
targetCursorPosition = textField.offset(from: textField.beginningOfDocument, to: startPosition)
}


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


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


let cardNumberWithSpaces = self.insertCreditCardSpaces(cardNumberWithoutSpaces, preserveCursorPosition: &targetCursorPosition)
textField.text = cardNumberWithSpaces


if let targetPosition = textField.position(from: textField.beginningOfDocument, offset: targetCursorPosition) {
textField.selectedTextRange = textField.textRange(from: targetPosition, to: targetPosition)
}
}


func removeNonDigits(string: String, andPreserveCursorPosition cursorPosition: inout Int) -> String {
var digitsOnlyString = ""
let originalCursorPosition = cursorPosition


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


return digitsOnlyString
}


func insertCreditCardSpaces(_ string: String, preserveCursorPosition cursorPosition: inout Int) -> String {
// Mapping of card prefix to pattern is taken from
// https://baymard.com/checkout-usability/credit-card-patterns


// UATP cards have 4-5-6 (XXXX-XXXXX-XXXXXX) format
let is456 = string.hasPrefix("1")


// These prefixes reliably indicate either a 4-6-5 or 4-6-4 card. We treat all these
// as 4-6-5-4 to err on the side of always letting the user type more digits.
let is465 = [
// Amex
"34", "37",


// Diners Club
"300", "301", "302", "303", "304", "305", "309", "36", "38", "39"
].contains { string.hasPrefix($0) }


// In all other cases, assume 4-4-4-4-3.
// This won't always be correct; for instance, Maestro has 4-4-5 cards according
// to https://baymard.com/checkout-usability/credit-card-patterns, but I don't
// know what prefixes identify particular formats.
let is4444 = !(is456 || is465)


var stringWithAddedSpaces = ""
let cursorPositionInSpacelessString = cursorPosition


for i in 0..<string.count {
let needs465Spacing = (is465 && (i == 4 || i == 10 || i == 15))
let needs456Spacing = (is456 && (i == 4 || i == 9 || i == 15))
let needs4444Spacing = (is4444 && i > 0 && (i % 4) == 0)


if needs465Spacing || needs456Spacing || needs4444Spacing {
stringWithAddedSpaces.append(" ")


if i < cursorPositionInSpacelessString {
cursorPosition += 1
}
}


let characterToAdd = string[string.index(string.startIndex, offsetBy:i)]
stringWithAddedSpaces.append(characterToAdd)
}


return stringWithAddedSpaces
}
}

将其适应于其他情况——比如您的代表不是 ViewController——留给读者作为练习。

创建新的快捷文件并粘贴到代码下面,将文本字段类更改为 VSTextField

import UIKit


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


public class VSTextField: UITextField {
    

/**
Set a formatting pattern for a number and define a replacement string. For example: If formattingPattern would be "##-##-AB-##" and
replacement string would be "#" and user input would be "123456", final string would look like "12-34-AB-56"
*/
public func setFormatting(_ formattingPattern: String, replacementChar: Character) {
self.formattingPattern = formattingPattern
self.replacementChar = replacementChar
self.formatting = .custom
}
    

/**
A character which will be replaced in formattingPattern by a number
*/
public var replacementChar: Character = "*"
    

/**
A character which will be replaced in formattingPattern by a number
*/
public var secureTextReplacementChar: Character = "\u{25cf}"
    

/**
True if input number is hexadecimal eg. UUID
*/
public var isHexadecimal: Bool {
return formatting == .uuid
}
    

/**
Max length of input string. You don't have to set this if you set formattingPattern.
If 0 -> no limit.
*/
public var maxLength = 0
    

/**
Type of predefined text formatting. (You don't have to set this. It's more a future feature)
*/
public var formatting : TextFieldFormatting = .noFormatting {
didSet {
switch formatting {
                

case .socialSecurityNumber:
self.formattingPattern = "***-**-****"
self.replacementChar = "*"
                

case .phoneNumber:
self.formattingPattern = "***-***-****"
self.replacementChar = "*"
                

case .uuid:
self.formattingPattern = "********-****-****-****-************"
self.replacementChar = "*"
                

default:
self.maxLength = 0
}
}
}
    

/**
String with formatting pattern for the text field.
*/
public var formattingPattern: String = "" {
didSet {
self.maxLength = formattingPattern.count
}
}
    

/**
Provides secure text entry but KEEPS formatting. All digits are replaced with the bullet character \u{25cf} .
*/
public var formatedSecureTextEntry: Bool {
set {
_formatedSecureTextEntry = newValue
super.isSecureTextEntry = false
}
        

get {
return _formatedSecureTextEntry
}
}
    

override public var text: String! {
set {
super.text = newValue
textDidChange() // format string properly even when it's set programatically
}
        

get {
if case .noFormatting = formatting {
return super.text
} else {
// Because the UIControl target action is called before NSNotificaion (from which we fire our custom formatting), we need to
// force update finalStringWithoutFormatting to get the latest text. Otherwise, the last character would be missing.
textDidChange()
return finalStringWithoutFormatting
}
}
}
    

required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
registerForNotifications()
}
    

override init(frame: CGRect) {
super.init(frame: frame)
registerForNotifications()
}
    

deinit {
NotificationCenter.default.removeObserver(self)
}
    

/**
Final text without formatting characters (read-only)
*/
public var finalStringWithoutFormatting : String {
return _textWithoutSecureBullets.keepOnlyDigits(isHexadecimal: isHexadecimal)
}
    

// MARK: - INTERNAL
fileprivate var _formatedSecureTextEntry = false
    

// if secureTextEntry is false, this value is similar to self.text
// if secureTextEntry is true, you can find final formatted text without bullets here
fileprivate var _textWithoutSecureBullets = ""
    

fileprivate func registerForNotifications() {
NotificationCenter.default.addObserver(self,
selector: #selector(VSTextField.textDidChange),
name: NSNotification.Name(rawValue: "UITextFieldTextDidChangeNotification"),
object: self)
}
    

@objc public func textDidChange() {
var superText: String { return super.text ?? "" }
        

// TODO: - Isn't there more elegant way how to do this?
let currentTextForFormatting: String
        

if superText.count > _textWithoutSecureBullets.count {
currentTextForFormatting = _textWithoutSecureBullets + superText[superText.index(superText.startIndex, offsetBy: _textWithoutSecureBullets.count)...]
} else if superText.count == 0 {
_textWithoutSecureBullets = ""
currentTextForFormatting = ""
} else {
currentTextForFormatting = String(_textWithoutSecureBullets[..<_textWithoutSecureBullets.index(_textWithoutSecureBullets.startIndex, offsetBy: superText.count)])
}
        

if formatting != .noFormatting && currentTextForFormatting.count > 0 && formattingPattern.count > 0 {
let tempString = currentTextForFormatting.keepOnlyDigits(isHexadecimal: isHexadecimal)
            

var finalText = ""
var finalSecureText = ""
            

var stop = false
            

var formatterIndex = formattingPattern.startIndex
var tempIndex = tempString.startIndex
            

while !stop {
let formattingPatternRange = formatterIndex ..< formattingPattern.index(formatterIndex, offsetBy: 1)
if formattingPattern[formattingPatternRange] != String(replacementChar) {
                

finalText = finalText + formattingPattern[formattingPatternRange]
finalSecureText = finalSecureText + formattingPattern[formattingPatternRange]
                    

} else if tempString.count > 0 {
                    

let pureStringRange = tempIndex ..< tempString.index(tempIndex, offsetBy: 1)
                    

finalText = finalText + tempString[pureStringRange]
                    

// we want the last number to be visible
if tempString.index(tempIndex, offsetBy: 1) == tempString.endIndex {
finalSecureText = finalSecureText + tempString[pureStringRange]
} else {
finalSecureText = finalSecureText + String(secureTextReplacementChar)
}
                    

tempIndex = tempString.index(after: tempIndex)
}
                

formatterIndex = formattingPattern.index(after: formatterIndex)
                

if formatterIndex >= formattingPattern.endIndex || tempIndex >= tempString.endIndex {
stop = true
}
}
            

_textWithoutSecureBullets = finalText
            

let newText = _formatedSecureTextEntry ? finalSecureText : finalText
if newText != superText {
super.text = _formatedSecureTextEntry ? finalSecureText : finalText
}
}
        

// Let's check if we have additional max length restrictions
if maxLength > 0 {
if superText.count > maxLength {
super.text = String(superText[..<superText.index(superText.startIndex, offsetBy: maxLength)])
_textWithoutSecureBullets = String(_textWithoutSecureBullets[..<_textWithoutSecureBullets.index(_textWithoutSecureBullets.startIndex, offsetBy: maxLength)])
}
}
}
}




extension String {
    

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




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


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

你可在以下连结找到更多用途:

以下是基于 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
}

然后在编辑文本上添加一个文本更改的侦听器:

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

斯威夫特5.1 Xcode 11

在尝试了许多解决方案之后,我遇到了一些问题,比如设置正确的光标位置和根据需要进行格式化,最后我在结合了2个帖子(https://stackoverflow.com/a/38838740/10579134https://stackoverflow.com/a/45297778/10579134)之后找到了一个解决方案

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




if textField == yourTextField  {


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


return false
}
return true
}

然后加上这个扩展

extension UITextField {


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

雨燕5:

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




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


if !replacementStringIsLegal {
return false
}


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


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


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


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


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


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

您可以使用我的简单库: DECardNumberFormatter

例如:

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

产出:

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


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

这是 Mark Amery 接受了这个答案Swift 5版本。

在类中添加以下变量:

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

还要确保您的文本字段从 viewDidLoad()调用 reformatAsCardNumber:

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

在你的 UITextFieldDelegate中加入以下内容:

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
   

if textField == cardNumberTextField {
previousTextFieldContent = textField.text;
previousSelection = textField.selectedTextRange;
}
    

return true
}

最后在你的 viewController中包括以下方法:

@objc func reformatAsCardNumber(textField: UITextField) {
var targetCursorPosition = 0
if let startPosition = textField.selectedTextRange?.start {
targetCursorPosition = textField.offset(from: textField.beginningOfDocument, to: startPosition)
}
    

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

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

let cardNumberWithSpaces = self.insertSpacesEveryFourDigitsIntoString(string: cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)
textField.text = cardNumberWithSpaces
    

if let targetPosition = textField.position(from: textField.beginningOfDocument, offset: targetCursorPosition) {
textField.selectedTextRange = textField.textRange(from: targetPosition, to: targetPosition)
}
}


func removeNonDigits(string: String, andPreserveCursorPosition cursorPosition: inout Int) -> String {
var digitsOnlyString = ""
let originalCursorPosition = cursorPosition
    

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

return digitsOnlyString
}


func insertSpacesEveryFourDigitsIntoString(string: String, andPreserveCursorPosition cursorPosition: inout Int) -> String {
var stringWithAddedSpaces = ""
let cursorPositionInSpacelessString = cursorPosition
    

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

return stringWithAddedSpaces
}

你可以使用 StringPatternFormatter吊舱:

pod 'StringPatternFormatter'

UITextField需要实施来自 UITextFieldDelegate的下列方法:

import StringPatternFormatter


...


textField.delegate = self


...


func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let text = textField.text else {
return true
}
    

let lastText = (text as NSString).replacingCharacters(in: range, with: string) as String
        

textField.text = lastText.format("nnnn nnnn nnnn nnnn", oldString: text)
return false
}
          ***Xcode 14, Swift 5.7:**
            

Below solution should work by considering cursor situations as well. In most of the answers when we edit cursor is moved to end of field.
                        

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let textFieldText = textField.text as NSString?
let value = textFieldText?.replacingCharacters(in: range, with: string) ?? ""
textField.setText(to: value.grouping(every: 4, with: " "), preservingCursor: true)
return false
}
                        

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

extension String {
                        

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