我想格式化一个UITextField
用于输入信用卡号码,只允许输入数字并自动插入空格,使得卡号的格式如下:
XXXX XXXX XXXX XXXX
我该怎么做?
我想格式化一个UITextField
用于输入信用卡号码,只允许输入数字并自动插入空格,使得卡号的格式如下:
XXXX XXXX XXXX XXXX
我该怎么做?
UITextFieldDelegate
添加这些实例变量...NSString *previousTextFieldContent;
UITextRange *previousSelection;
...和这些方法:
// Version 1.3
// Source and explanation: https://dev59.com/E2ct5IYBdhLWcg3wc9Eg#19161529
-(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卡也可以少于16位数字,而Maestro卡可以有更多的数字:
用户与文本字段交互的方式不仅仅是在现有输入的末尾键入单个字符。您还必须正确处理用户在字符串中间添加字符、删除单个字符、删除多个选定字符和粘贴多个字符的情况。对于这个问题来说,一些更简单/更幼稚的方法将无法正确处理其中的一些交互。最恶劣的情况是用户在字符串中间粘贴多个字符以替换其他字符,而此解决方案足以处理这种情况。
在用户修改文本字段的文本后,您不仅需要正确重新格式化文本字段的文本,还需要合理地定位文本光标。不考虑这一点的问题的幼稚方法几乎肯定会在某些情况下做一些愚蠢的事情,比如在用户在其中添加数字后将其放置到文本字段的末尾。
可能对于这些问题的任何答案都是足够的,但我列出它们只是为了明确,在这里你可能需要仔细考虑很多特殊情况,如果你够着迷的话。在上面的代码中,我选择了我认为合理的这些问题的答案。如果你对其中任何一点有强烈的感受,而这些感受与我的代码行为不兼容,那么调整它以满足你的需求应该很容易。
targetCursorPosition
的方式是错误的。尝试添加一个不是Unicode标量的字符,比如表情符号。这样会导致光标位置不准确。 - HHKMain.storyboard
上添加一个“Text Field”。
3. 将ViewController
设置为Text Field的委托。
4. 将下面的代码粘贴到ViewController.swift
中。
5. 连接IBOutlet
到Text Field。
6. 运行您的应用程序并在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($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
,留给读者作为练习。
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSBigMutableString substringWithRange:]: Range {20, 0} out of bounds; string length 19'
- Josh ParadroidreformatAsCardNumber
结束时加上 textField.undoManager?.removeAllActions()
可以防止崩溃发生。虽然不是最好的解决方法,但它可以解决问题。 - Josh Paradroid你可能能够优化我的代码,或者可能有更简单的方法,但是这段代码应该可以工作:
-(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;
}
使用Fawkes的答案作为基础的Swift 3解决方案。 新增Amex卡片格式支持。 当卡片类型改变时,新增重组。
首先用以下代码创建新类:
extension String {
func containsOnlyDigits() -> Bool
{
let notDigits = NSCharacterSet.decimalDigits.inverted
if rangeOfCharacter(from: notDigits, options: String.CompareOptions.literal, range: nil) == nil
{
return true
}
return false
}
}
import UIKit
var creditCardFormatter : CreditCardFormatter
{
return CreditCardFormatter.sharedInstance
}
class CreditCardFormatter : NSObject
{
static let sharedInstance : CreditCardFormatter = CreditCardFormatter()
func formatToCreditCardNumber(isAmex: Bool, textField : UITextField, withPreviousTextContent previousTextContent : String?, andPreviousCursorPosition previousCursorSelection : UITextRange?) {
var selectedRangeStart = textField.endOfDocument
if textField.selectedTextRange?.start != nil {
selectedRangeStart = (textField.selectedTextRange?.start)!
}
if let textFieldText = textField.text
{
var targetCursorPosition : UInt = UInt(textField.offset(from:textField.beginningOfDocument, to: selectedRangeStart))
let cardNumberWithoutSpaces : String = removeNonDigitsFromString(string: textFieldText, andPreserveCursorPosition: &targetCursorPosition)
if cardNumberWithoutSpaces.characters.count > 19
{
textField.text = previousTextContent
textField.selectedTextRange = previousCursorSelection
return
}
var cardNumberWithSpaces = ""
if isAmex {
cardNumberWithSpaces = insertSpacesInAmexFormat(string: cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)
}
else
{
cardNumberWithSpaces = insertSpacesIntoEvery4DigitsIntoString(string: cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)
}
textField.text = cardNumberWithSpaces
if let finalCursorPosition = textField.position(from:textField.beginningOfDocument, offset: Int(targetCursorPosition))
{
textField.selectedTextRange = textField.textRange(from: finalCursorPosition, to: finalCursorPosition)
}
}
}
func removeNonDigitsFromString(string : String, andPreserveCursorPosition cursorPosition : inout UInt) -> String {
var digitsOnlyString : String = ""
for index in stride(from: 0, to: string.characters.count, by: 1)
{
let charToAdd : Character = Array(string.characters)[index]
if isDigit(character: charToAdd)
{
digitsOnlyString.append(charToAdd)
}
else
{
if index < Int(cursorPosition)
{
cursorPosition -= 1
}
}
}
return digitsOnlyString
}
private func isDigit(character : Character) -> Bool
{
return "\(character)".containsOnlyDigits()
}
func insertSpacesInAmexFormat(string : String, andPreserveCursorPosition cursorPosition : inout UInt) -> String {
var stringWithAddedSpaces : String = ""
for index in stride(from: 0, to: string.characters.count, by: 1)
{
if index == 4
{
stringWithAddedSpaces += " "
if index < Int(cursorPosition)
{
cursorPosition += 1
}
}
if index == 10 {
stringWithAddedSpaces += " "
if index < Int(cursorPosition)
{
cursorPosition += 1
}
}
if index < 15 {
let characterToAdd : Character = Array(string.characters)[index]
stringWithAddedSpaces.append(characterToAdd)
}
}
return stringWithAddedSpaces
}
func insertSpacesIntoEvery4DigitsIntoString(string : String, andPreserveCursorPosition cursorPosition : inout UInt) -> String {
var stringWithAddedSpaces : String = ""
for index in stride(from: 0, to: string.characters.count, by: 1)
{
if index != 0 && index % 4 == 0 && index < 16
{
stringWithAddedSpaces += " "
if index < Int(cursorPosition)
{
cursorPosition += 1
}
}
if index < 16 {
let characterToAdd : Character = Array(string.characters)[index]
stringWithAddedSpaces.append(characterToAdd)
}
}
return stringWithAddedSpaces
}
}
在您的ViewControllerClass中添加此函数。
func reformatAsCardNumber(textField:UITextField){
let formatter = CreditCardFormatter()
var isAmex = false
if selectedCardType == "AMEX" {
isAmex = true
}
formatter.formatToCreditCardNumber(isAmex: isAmex, textField: textField, withPreviousTextContent: textField.text, andPreviousCursorPosition: textField.selectedTextRange)
}
然后将目标添加到您的textField中
youtTextField.addTarget(self, action: #selector(self.reformatAsCardNumber(textField:)), for: UIControlEvents.editingChanged)
注册新变量并将卡类型发送到该变量
var selectedCardType: String? {
didSet{
reformatAsCardNumber(textField: yourTextField)
}
}
感谢Fawkes提供的代码!
1234 5678 9012
,我将光标定位在 9
后面并按退格键,则 9
将被删除,但我的文本光标最终会停留在 8
后面而不是 0
后面。 - Mark Amery我认为这个不错:
-(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;
}
if ([string length] == 0) return YES;
- Lucas所以我希望用更少的代码来完成这个任务,于是我使用了这里的代码,并加以修改。屏幕上有两个字段,一个是数字,一个是到期日期,因此我让它更具可重用性。
Swift 3 替代方案
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let currentText = (textField.text as NSString?)?.replacingCharacters(in: range, with: string) else { return true }
if textField == cardNumberTextField {
textField.text = currentText.grouping(every: 4, with: " ")
return false
}
else { // Expiry Date Text Field
textField.text = currentText.grouping(every: 2, with: "/")
return false
}
}
extension String {
func grouping(every groupSize: String.IndexDistance, with separator: Character) -> String {
let cleanedUpCopy = replacingOccurrences(of: String(separator), with: "")
return String(cleanedUpCopy.characters.enumerated().map() {
$0.offset % groupSize == 0 ? [separator, $0.element] : [$0.element]
}.joined().dropFirst())
}
}
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
{
if textField == CardNumTxt
{
let replacementStringIsLegal = string.rangeOfCharacterFromSet(NSCharacterSet(charactersInString: "0123456789").invertedSet) == nil
if !replacementStringIsLegal
{
return false
}
let newString = (textField.text! as NSString).stringByReplacingCharactersInRange(range, withString: string)
let components = newString.componentsSeparatedByCharactersInSet(NSCharacterSet(charactersInString: "0123456789").invertedSet)
let decimalString = components.joinWithSeparator("") as NSString
let length = decimalString.length
let hasLeadingOne = length > 0 && decimalString.characterAtIndex(0) == (1 as unichar)
if length == 0 || (length > 16 && !hasLeadingOne) || length > 19
{
let newLength = (textField.text! as NSString).length + (string as NSString).length - range.length as Int
return (newLength > 16) ? false : true
}
var index = 0 as Int
let formattedString = NSMutableString()
if hasLeadingOne
{
formattedString.appendString("1 ")
index += 1
}
if length - index > 4
{
let prefix = decimalString.substringWithRange(NSMakeRange(index, 4))
formattedString.appendFormat("%@-", prefix)
index += 4
}
if length - index > 4
{
let prefix = decimalString.substringWithRange(NSMakeRange(index, 4))
formattedString.appendFormat("%@-", prefix)
index += 4
}
if length - index > 4
{
let prefix = decimalString.substringWithRange(NSMakeRange(index, 4))
formattedString.appendFormat("%@-", prefix)
index += 4
}
let remainder = decimalString.substringFromIndex(index)
formattedString.appendString(remainder)
textField.text = formattedString as String
return false
}
else
{
return true
}
}
formattedString.appendFormat("%@-", prefix) 中的“-”可以改成你想要的任何其他字符。
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
}
}
这里是 Swift 5 版本的 Mark Amery所接受的答案。
在你的类中添加这些变量:
@IBOutlet weak var cardNumberTextField: UITextField!
private var previousTextFieldContent: String?
private var previousSelection: UITextRange?
reformatAsCardNumber
:从 viewDidLoad()
:。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
}
以下是针对Swift 2的已接受答案的另一个版本...
请确保在您的代理实例中包含以下内容:
private var previousTextFieldContent: String?
private var previousSelection: UITextRange?
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
}