我想格式化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方法是一个合适的位置.).

一些解释

这是一个看似复杂的问题.三个重要的问题可能不会立即显而易见(之前的答案都没有考虑到):

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

    美国运通卡

    即使是Visa卡也可以有fewer than个16位数字,而Maestro卡可以有更多:

    一张18位数的俄罗斯大师卡

  2. 用户可以通过多种方式与文本字段交互,而不仅仅是在现有输入的末尾键入单个字符.您还必须正确处理字符串的用户adding characters in the middle、删除多个选定字符的deleting个单字符和多个字符的pasting个.这个问题的一些更简单/更幼稚的方法将无法正确处理其中一些交互.最反常的情况是用户在字符串中间粘贴多个字符来替换其他字符,此解决方案非常通用,足以处理此问题.

  3. 用户修改文本字段后,不仅需要正确地重新格式化文本字段的文本,还需要合理地定位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更新刷新到屏幕之前被调用,所以这种方法工作得很好.

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

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

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

Ios相关问答推荐

更新Flutter项目时出错:CocoaPods找不到pod webview_flutter_wkwebview的兼容版本""

Xamarin Forms CustomPin iOS Render

如何防止UITest套件与Fastlane一起执行?

许多文本字段的性能问题

Flutter应用内购买无法恢复购买

在 Swift 项目中使用情节提要加载 obj-c 文件

Swift UI中如何为Text提供内部填充

如何在 SwiftUI 中通过通用 @ObservedObject 进行切换?

uikit中如何触发swiftui功能

CGPath intersection(_:using:) 和朋友(iOS 16 中的新功能)坏了?

占位符文本未显示在 TextEditor 上

在 SwiftUI 中打开 PDF

我可以在 Apple-Watch 上使用 iPhone 的摄像头扫描 SwiftUi 中的二维码(例如用于登录)吗

在 tableview 中下载图像缩略图时如何提高滚动性能

UIButton在iOS7中没有显示突出显示

检测 iPhone/iPad/iPod touch 的 colored颜色 ?

如何使用 Swift 从assets资源 中加载特定图像

如何找出用于构建 *.ipa 文件的配置文件?

如何获取 iTunes 连接团队 ID 和团队名称?

UISegmentedControl 以编程方式更改段数