ios 为信用卡输入格式化 UITextField,如 (xxxx xxxx xxxx xxxx)
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/12083605/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Formatting a UITextField for credit card input like (xxxx xxxx xxxx xxxx)
提问by Can Aksoy
I want to format a UITextField
for entering a credit card number into such that it only allows digits to be entered and automatically inserts spaces so that the number is formatted like so:
我想格式化 aUITextField
以输入信用卡号,以便它只允许输入数字并自动插入空格,以便数字格式如下:
XXXX XXXX XXXX XXXX
How can I do this?
我怎样才能做到这一点?
回答by Mark Amery
If you're using Swift, go read my port of this answer for Swift 4and use that instead.
如果您使用的是 Swift,请阅读我为 Swift 4 提供的这个答案的端口,然后改用它。
If you're in Objective-C...
如果你在 Objective-C 中...
Firstly, to your UITextFieldDelegate
, add these instance variables...
首先,到您的UITextFieldDelegate
,添加这些实例变量...
NSString *previousTextFieldContent;
UITextRange *previousSelection;
... and these methods:
...以及这些方法:
// 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;
}
Secondly, set reformatCardNumber:
to be called whenever the text field fires a UIControlEventEditingChanged
event:
其次,设置reformatCardNumber:
为每当文本字段触发UIControlEventEditingChanged
事件时调用:
[yourTextField addTarget:yourTextFieldDelegate
action:@selector(reformatAsCardNumber:)
forControlEvents:UIControlEventEditingChanged];
(Of course, you'll need to do this at some point after your text field and its delegate have been instantiated. If you're using storyboards, the viewDidLoad
method of your view controller is an appropriate place.
(当然,您需要在文本字段及其委托实例化后的某个时间执行此操作。如果您使用的是故事板,则viewDidLoad
视图控制器的方法是一个合适的地方。
Some Explanation
一些解释
This is a deceptively complicated problem. Three important issues that may not be immediately obvious (and which previous answers here all fail to take into account):
这是一个看似复杂的问题。三个可能不是很明显的重要问题(这里以前的答案都没有考虑到):
While the
XXXX XXXX XXXX XXXX
format for credit and debit card numbers is the most common one, it's not the only one. For example, American Express cards have 15 digit numbers usually written inXXXX XXXXXX XXXXX
format, like this:Even Visa cards can have fewer than16 digits, and Maestro cards can have more:
There are more ways for the user to interact with a text field than just typing in single characters at the end of their existing input. You also have to properly handle the user adding characters in the middleof the string, deletingsingle characters, deleting multiple selected characters, and pastingin multiple characters. Some simpler/more naive approaches to this problem will fail to handle some of these interactions properly. The most perverse case is a user pasting in multiple characters in the middle of the string to replace other characters, and this solution is general enough to handle that.
You don't just need to reformat the text of the text field properly after the user modifies it - you also need to position the text cursorsensibly. Naive approaches to the problem that don't take this into account will almost certainly end up doing something silly with the text cursor in some cases (like putting it to the end of the text field after the user adds a digit in the middle of it).
虽然
XXXX XXXX XXXX XXXX
信用卡和借记卡号码的格式是最常见的一种,但它并不是唯一的。例如,美国运通卡有 15 位数字,通常以XXXX XXXXXX XXXXX
这种格式书写,如下所示:即使是 Visa 卡也可以少于16 位,而 Maestro 卡可以有更多:
用户可以通过更多方式与文本字段进行交互,而不仅仅是在现有输入的末尾输入单个字符。您还必须正确处理用户在字符串中间添加字符、删除单个字符、删除多个选定字符以及粘贴多个字符。解决这个问题的一些更简单/更幼稚的方法将无法正确处理其中一些交互。最反常的情况是用户在字符串中间粘贴多个字符来替换其他字符,这个解决方案足以处理这种情况。
您不仅需要在用户修改文本字段后正确地重新格式化文本字段的文本 - 您还需要合理地定位文本光标。在某些情况下,不考虑这一点的幼稚方法几乎肯定会用文本光标做一些愚蠢的事情(例如在用户在文本字段中间添加一个数字后将其放在文本字段的末尾) )。
To deal with issue #1, we use the partial mapping of card number prefixes to formats curated by The Baymard Institute at https://baymard.com/checkout-usability/credit-card-patterns. We can automatically detect the the card provider from the first couple of digits and (in somecases) infer the format and adjust our formatting accordingly. Thanks to cnotethegr8for contributing this idea to this answer.
为了解决问题 #1,我们使用卡号前缀的部分映射到由 Baymard Institute 在https://baymard.com/checkout-usability/credit-card-patterns策划的格式。我们可以从前几位数字自动检测卡提供商,并(在某些情况下)推断格式并相应地调整我们的格式。感谢cnotethegr8为这个答案贡献了这个想法。
The simplest and easiest way to deal with issue #2(and the way used in the code above) is to strip out all spaces and reinsert them in the correct positions every time the content of the text field changes, sparing us the need to figure out what kind of text manipulation (an insertion, a deletion, or a replacement) is going on and handle the possibilities differently.
处理问题#2的最简单和最简单的方法(以及上面代码中使用的方法)是每次文本字段的内容发生变化时,去掉所有空格并将它们重新插入正确的位置,这样我们就不需要计算了找出正在进行的文本操作(插入、删除或替换)的类型,并以不同的方式处理可能性。
To deal with issue #3, we keep track of how the desired index of the cursor changes as we strip out non-digits and then insert spaces. This is why the code rather verbosely performs these manipulations character-by-character using NSMutableString
, rather than using NSString
's string replacement methods.
为了解决问题#3,我们会跟踪光标的所需索引如何在我们去除非数字然后插入空格时发生变化。这就是为什么代码使用NSMutableString
,而不是使用NSString
的字符串替换方法逐个字符地执行这些操作的原因。
Finally, there's one more trap lurking: returning NO
from textField: shouldChangeCharactersInRange: replacementString
breaks the 'Cut' button the user gets when they select text in the text field, which is why I don't do it. Returning NO
from that method results in 'Cut' simply not updating the clipboard at all, and I know of no fix or workaround. As a result, we need to do the reformatting of the text field in a UIControlEventEditingChanged
handler instead of (more obviously) in shouldChangeCharactersInRange:
itself.
最后,还有一个陷阱潜伏着:NO
从textField: shouldChangeCharactersInRange: replacementString
中断返回用户在文本字段中选择文本时获得的“剪切”按钮,这就是我不这样做的原因。NO
从该方法返回导致“剪切”根本不更新剪贴板,我知道没有修复或解决方法。因此,我们需要在UIControlEventEditingChanged
处理程序中重新格式化文本字段,而不是(更明显)shouldChangeCharactersInRange:
本身。
Luckily, the UIControl event handlers seem to get called before UI updates get flushed to the screen, so this approach works fine.
幸运的是,UIControl 事件处理程序似乎在 UI 更新刷新到屏幕之前被调用,所以这种方法工作正常。
There are also a whole bunch of minor questions about exactly how the text field should behave that don't have obvious correct answers:
还有一大堆关于文本字段应该如何表现的小问题,但没有明显的正确答案:
- If the user tries to paste in something that would cause the content of the text field to exceed 19 digits, should the beginning of the pasted string be inserted (until 19 digits are reached) and the remainder cropped, or should nothing be inserted at all?
- If the user tries to delete a single space by positioning their cursor after it and pressing the backspace key, should nothing happen and the cursor remain where it is, should the cursor move left one character (placing it before the space), or should the digit to the left of the space be deleted as though the cursor were already left of the space?
- When the user types in the fourth, eighth, or twelfth digit, should a space be immediately inserted and the cursor moved after it, or should the space only be inserted after the user types the fifth, ninth, or thirteenth digit?
- When the user deletes the first digit after a space, if this doesn't cause the space to be removed entirely, should this lead to their cursor being positioned before or after the space?
- 如果用户尝试粘贴会导致文本字段内容超过 19 位的内容,是否应插入粘贴字符串的开头(直到达到 19 位)并裁剪其余部分,或者根本不插入任何内容?
- 如果用户试图通过将光标置于其后并按退格键来删除单个空格,如果没有任何反应并且光标保持在原处,光标是否应该向左移动一个字符(将其放在空格之前),或者应该空格左边的数字被删除,就好像光标已经在空格的左边一样?
- 当用户键入第四、第八或第十二位数字时,是否应立即插入空格并将光标移至其后,还是应在用户键入第五、第九或第十三位数字后插入空格?
- 当用户删除空格后的第一个数字时,如果这不会导致空格被完全删除,这是否会导致他们的光标定位在空格之前或之后?
Probably any answer to any of these questions will be adequate, but I list them just to make clear that there are actually a lot of special cases that you might want to think carefully about here, if you were obsessive enough. In the code above, I've picked answers to these questions that seemed reasonable to me. If you happen to have strong feelings about any of these points that aren't compatible with the way my code behaves, it should be easy enough to tweak it to your needs.
可能对这些问题中的任何一个的任何答案都足够了,但我列出它们只是为了表明实际上有很多特殊情况,如果您足够执着的话,您可能需要在这里仔细考虑。在上面的代码中,我选择了对我来说合理的这些问题的答案。如果您碰巧对这些与我的代码行为方式不兼容的任何一点有强烈的感觉,那么根据您的需要对其进行调整应该很容易。
回答by Mark Amery
Below is a working Swift 4 port of Logicopolis's answer(which is in turn a Swift 2 port of an old version of my accepted answerin Objective-C) enhanced with cnotethegr8 's trick for supporting Amex cards and then further enhanced to support more card formats. I suggest looking over the accepted answer if you haven't already, since it helps explain the motivation behind a lot of this code.
下面是Logicopolis 答案的一个有效Swift 4 端口(这又是我在Objective-C 中接受的旧版本答案的Swift 2 端口)通过cnotethegr8 支持美国运通卡的技巧得到增强,然后进一步增强以支持更多卡格式。如果您还没有,我建议您查看已接受的答案,因为它有助于解释很多此代码背后的动机。
Note that the minimal series of steps needed to see this in action is:
请注意,查看此操作所需的最小系列步骤是:
- Create a new Single View Appin Swift.
- On
Main.storyboard
, add a Text Field. - Make the
ViewController
the delegate of the Text Field. - Paste the code below into
ViewController.swift
. - Connect the
IBOutlet
to the Text Field. - Run your app and type in the Text Field.
- 在 Swift 中创建一个新的单一视图应用程序。
- 在 上
Main.storyboard
,添加一个文本字段。 - 制作Text Field
ViewController
的委托。 - 将下面的代码粘贴到
ViewController.swift
. - 连接
IBOutlet
到文本字段。 - 运行您的应用程序并在Text Field 中输入。
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(-(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;
}
) }
// 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
}
}
Adapting this to other situations - like your delegate not being a ViewController
- is left as an exercise for the reader.
将此适应其他情况——比如你的代表不是ViewController
——留给读者作为练习。
回答by Sebrassi
You can probably optimize my code or there might be an easier way but this code should work:
您可能可以优化我的代码,或者可能有更简单的方法,但此代码应该可以工作:
-(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;
}
回答by Lucas
I think this one is good:
我觉得这个不错:
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
}
}
回答by Dmitry
Swift 3solution using Fawkesanswer as basic. Added Amex Card format support. Added reformation when card type changed.
使用Fawkes 的Swift 3解决方案作为基本答案。添加了美国运通卡格式支持。当卡类型改变时增加了改革。
First make new class with this code:
首先使用此代码创建新类:
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)
}
In your ViewControllerClass add this function
在您的 ViewControllerClass 中添加此功能
youtTextField.addTarget(self, action: #selector(self.reformatAsCardNumber(textField:)), for: UIControlEvents.editingChanged)
Then add target to your textField
然后将目标添加到您的 textField
var selectedCardType: String? {
didSet{
reformatAsCardNumber(textField: yourTextField)
}
}
Register new variable and sent card type to it
注册新变量并将卡类型发送给它
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
}
}
Thanks Fawkes for his code!
感谢 Fawkes 的代码!
回答by Jayesh Miruliya
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() {
private var previousTextFieldContent: String?
private var previousSelection: UITextRange?
.offset % groupSize == 0 ? [separator, textField.addTarget(self, action: #selector(reformatAsCardNumber(_:)), forControlEvents: .EditingChanged)
.element] : [func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
previousTextFieldContent = textField.text;
previousSelection = textField.selectedTextRange;
return true
}
.element]
}.joined().dropFirst())
}
}
formattedString.appendFormat("%@-", prefix) chage of "-" any other your choose
formattedString.appendFormat("%@-", prefix) 更改 "-" 任何其他您选择的
回答by Darvish Kamalia
So I wanted to this with less code, so I used the code hereand repurposed it a little bit. I had two fields in the screen, one for the number and one for the expiry date, so I made it more reusable.
所以我想用更少的代码来做到这一点,所以我在这里使用了代码并重新调整了它的用途。我在屏幕上有两个字段,一个是数字,一个是到期日,所以我让它更容易重复使用。
Swift 3 alternate answer
斯威夫特 3 替代答案
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
}
回答by Logicopolis
Yet another version of the accepted answer in Swift 2...
Swift 2 中接受的答案的另一个版本......
Ensure you have these in your delegate instance:
确保您的委托实例中有这些:
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
}
And also ensure that your text field calls reformatAsCardNumber:
并确保您的文本字段调用 reformatAsCardNumber:
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
}
You text field delegate will need to do this:
您的文本字段委托需要执行以下操作:
##代码##Lastly include the following methods:
最后包括以下方法:
##代码##回答by Sleeping_Giant
Here is a Swift version in case this is useful to anyone still looking for this answer but using Swift instead of Objective-C. The concepts are still the same regardless.
这是一个 Swift 版本,以防它对仍在寻找此答案但使用 Swift 而不是 Objective-C 的任何人有用。无论如何,概念仍然相同。
##代码##回答by umair151
Swift 3.2
斯威夫特 3.2
Little correction in the @Lucas answer and working code in swift 3.2. Also removing the space character automatically.
@Lucas 答案和 swift 3.2 中的工作代码中的小修正。还会自动删除空格字符。
##代码##