我想格式化UITextField
,以便在其中输入信用卡号码,这样它只允许输入数字,并自动插入空格,因此数字的格式如下:
XXXX XXXX XXXX XXXX
我怎么才能做到这一点呢?
我想格式化UITextField
,以便在其中输入信用卡号码,这样它只允许输入数字,并自动插入空格,因此数字的格式如下:
XXXX XXXX XXXX XXXX
我怎么才能做到这一点呢?
如果你使用的是SWIFT,那就go 读my port of this answer for Swift 4,然后用它来代替.
如果你在Objective-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
方法是一个合适的位置.).
这是一个看似复杂的问题.三个重要的问题可能不会立即显而易见(之前的答案都没有考虑到):
虽然信用卡和借记卡号码的XXXX XXXX XXXX XXXX
格式是最常见的格式,但它并不是唯一的格式.例如,美国运通卡有15位数字,通常以XXXX XXXXXX XXXXX
格式书写,如下所示:
即使是Visa卡也可以有fewer than个16位数字,而Maestro卡可以有更多:
用户可以通过多种方式与文本字段交互,而不仅仅是在现有输入的末尾键入单个字符.您还必须正确处理字符串的用户adding characters in the middle、删除多个选定字符的deleting个单字符和多个字符的pasting个.这个问题的一些更简单/更幼稚的方法将无法正确处理其中一些交互.最反常的情况是用户在字符串中间粘贴多个字符来替换其他字符,此解决方案非常通用,足以处理此问题.
用户修改文本字段后,不仅需要正确地重新格式化文本字段的文本,还需要合理地定位text cursor.不考虑这一问题的天真的方法几乎肯定会在某些情况下用文本游标做一些傻事(比如在用户添加一个数字在中间时,把它放在文本字段的末尾).
为了解决问题#1,我们使用卡号前缀到Baymard Institute在https://baymard.com/checkout-usability/credit-card-patterns处策划的格式的部分映射.我们可以从头几个数字自动检测卡Provider ,并(在some个情况下)推断格式并相应地调整我们的格式.感谢cnotethegr8为这个答案贡献了这个 idea .
处理问题#2的最简单、最容易的方法(以及上面代码中使用的方法)是,每当文本字段的内容发生更改时,go 掉所有空格并将它们重新插入到正确的位置,这样我们就不必找出正在进行哪种文本操作(插入、删除或替换),并以不同的方式处理这些可能性.
为了解决问题#3,我们在go 掉非数字,然后插入空格时,跟踪光标所需索引的变化.这就是为什么代码使用NSMutableString
而不是NSString
的字符串替换方法逐字执行这些操作的原因.
最后,还有一个潜在的trap :从textField: shouldChangeCharactersInRange: replacementString
返回NO
会 destruct 用户在文本字段中 Select 文本时得到的"剪切"按钮,这就是我不这么做的原因.从该方法返回NO
会导致"剪切"根本不更新剪贴板,我知道没有修复或解决方法.因此,我们需要在UIControlEventEditingChanged
处理程序中重新格式化文本字段,而不是(更明显地)在shouldChangeCharactersInRange:
本身中.
幸运的是,UIControl事件处理程序似乎在UI更新刷新到屏幕之前被调用,所以这种方法工作得很好.
还有一大堆关于文本字段应该如何运行的小问题,没有明显的正确答案:
也许这些问题的任何答案都是足够的,但我列出它们只是为了说明,如果你足够痴迷的话,实际上有很多特殊情况你可能需要仔细考虑.在上面的代码中,我 Select 了这些对我来说似乎合理的问题的答案.如果您碰巧对这些与我的代码行为方式不兼容的点有强烈的感觉,那么根据您的需要调整它应该足够容易.