Подтвердить что ты не робот

Форматирование UITextField для ввода кредитной карты типа (xxxx xxxx xxxx xxxx)

Я хочу отформатировать UITextField для ввода номера кредитной карты, чтобы он позволял вводить цифры и автоматически вставлять пробелы, чтобы число было отформатировано следующим образом:

XXXX XXXX XXXX XXXX

Как я могу это сделать?

4b9b3361

Ответ 1

Если вы используете Swift, прочитайте мой порт этого ответа для 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 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 для номеров кредитных и дебетовых карт является наиболее распространенным, он не единственный. Например, карты American Express имеют 15-значные номера, обычно записанные в формате XXXX XXXXXX XXXXX, например:

    An American Express card

    Даже карты Visa могут содержать не более 16 цифр, а карты Maestro могут иметь больше:

    A Russian Maestro card with 18 digits

  2. Для пользователя существует больше способов взаимодействия с текстовым полем, чем просто ввод отдельных символов в конце существующего ввода. Вам также необходимо правильно обрабатывать добавление символов в середине строки, удаление отдельных символов, удаление нескольких выбранных символов и вставку нескольких символов. Некоторые более простые/более наивные подходы к этой проблеме не смогут правильно обрабатывать некоторые из этих взаимодействий. Наиболее извращенным случаем является вставка пользователем нескольких символов в середине строки для замены других символов, и это решение достаточно общее, чтобы справиться с этим.

  3. Вам не нужно просто правильно переформатировать текст текстового поля после того, как пользователь его модифицирует - вам также нужно разумно расположить текстовый курсор. Наивные подходы к проблеме, которые не учитывают это, почти наверняка в некоторых случаях приведут к глупым действиям с текстовым курсором (например, к концу текстового поля после того, как пользователь добавит цифру в середине).).

Чтобы решить проблему № 1, мы используем частичное сопоставление префиксов номеров карт форматам, отобранным The Baymard Institute по адресу https://baymard.com/checkout-usability/credit-card-patterns. Мы можем автоматически определить поставщика карты по первой паре цифр и (в некоторых случаях) определить формат и соответствующим образом изменить наше форматирование. Спасибо cnotethegr8 за поддержку этой идеи в этом ответе.

Самый простой и легкий способ справиться с проблемой № 2 (и способом, использованным в приведенном выше коде) - это убрать все пробелы и вставлять их в правильные позиции каждый раз, когда изменяется содержимое текстового поля, избавляя нас от необходимости разбираться выяснить, какой вид манипуляции с текстом (вставка, удаление или замена) происходит и обрабатывать возможности по-разному.

Чтобы справиться с проблемой № 3, мы отслеживаем, как изменяется требуемый индекс курсора, когда мы удаляем нецифровые символы, а затем вставляем пробелы. Вот почему код довольно многословно выполняет эти манипуляции посимвольно, используя NSMutableString, а не используя методы замены строк NSString.

Наконец, есть еще одна ловушка: возвращение NO из textField: shouldChangeCharactersInRange: replacementString ломает кнопку "Вырезать", которую получает пользователь, когда выбирает текст в текстовом поле, поэтому я этого не делаю. Возврат NO из этого метода приводит к тому, что "Cut" просто не обновляет буфер обмена, и я не знаю ни исправления, ни обходного пути. В результате нам нужно выполнить переформатирование текстового поля в обработчике UIControlEventEditingChanged вместо (более очевидно) в shouldChangeCharactersInRange: самой.

К счастью, кажется, что обработчики событий UIControl вызываются до того, как обновления пользовательского интерфейса выводятся на экран, поэтому этот подход работает нормально.

Есть также целый ряд небольших вопросов о том, как именно должно вести себя текстовое поле, которые не имеют очевидных правильных ответов:

  • Если пользователь пытается вставить что-то, что может привести к тому, что содержимое текстового поля превысит 19 цифр, следует ли вставить вставленную строку (до достижения 19 цифр), а оставшуюся часть обрезать или вообще ничего не вставлять?
  • Если пользователь пытается удалить один пробел, поместив курсор за ним и нажав клавишу возврата, ничего не должно произойти, и курсор останется на своем месте, если курсор переместится влево на один символ (поместив его перед пробелом), или если цифру слева от пробела удалить, как если бы курсор уже был оставлен пробела?
  • Когда пользователь вводит четвертую, восьмую или двенадцатую цифру, должен ли сразу вставляться пробел и после него перемещается курсор, или пробел должен вставляться только после того, как пользователь вводит пятую, девятую или тринадцатую цифру?
  • Когда пользователь удаляет первую цифру после пробела, если это не приводит к полному удалению пробела, должно ли это привести к тому, что его курсор будет расположен до или после пробела?

Возможно, любой ответ на любой из этих вопросов будет адекватным, но я перечисляю их просто для того, чтобы прояснить, что на самом деле существует множество особых случаев, о которых вы могли бы подумать, если бы вы были достаточно одержимы. В приведенном выше коде я выбрал ответы на эти вопросы, которые показались мне разумными. Если вы испытываете сильные чувства к любому из этих пунктов, которые не совместимы с поведением моего кода, должно быть достаточно легко настроить его под ваши нужды.

Ответ 2

Вероятно, вы можете оптимизировать мой код, иначе может быть проще, но этот код должен работать:

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

Ответ 3

Ниже приведен рабочий порт ответа Swift 4 от Logicopolis (который, в свою очередь, является портом Swift 2 старой версии моего принятого ответа в Objective-C), расширен с помощью трюка cnotethegr8 для поддержки карт Amex, а затем улучшен для поддержки большего количества форматов карт. Я предлагаю просмотреть принятый ответ, если вы еще этого не сделали, поскольку он помогает объяснить мотивы, лежащие в основе большей части этого кода.

Обратите внимание, что минимальная последовательность шагов, необходимых для того, чтобы увидеть это в действии:

  1. Создайте новое приложение Single View в 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 - оставлен читателю в качестве упражнения.

Ответ 4

Я думаю, что это хорошо:

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

Ответ 5

Решение

Swift 3 с использованием Fawkes отвечает как базовое. Добавлена ​​поддержка формата карты 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)
}

Затем добавьте цель в свой текстовый элемент

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

Зарегистрировать новую переменную и тип отправленной карты

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

Спасибо Fawkes за его код!

Ответ 6

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) chage из "-" любой другой ваш выбор

Ответ 7

Еще одна версия принятого ответа в Swift 2...

Убедитесь, что они есть в экземпляре делегата:

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

И также убедитесь, что ваше текстовое поле вызывает reformatAsCardNumber:

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
}

Ответ 8

Вот версия 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
}

Ответ 9

Так что я хотел этого с меньшим количеством кода, поэтому я использовал код здесь и немного перепрофилировал его. У меня было два поля на экране: один для числа и один для даты истечения срока действия, поэтому я сделал его более многоразовым.

Альтернативный ответ 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())
    }
}

Ответ 10

Определите метод ниже и вызовите его в делегатах 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;
}

Ответ 11

Чтобы достичь цели форматирования текста, введенного в текстовое поле таким образом XXXX XXXX XXXX XXXX важно помнить о некоторых важных вещах. Помимо того факта, что 16-значный номер карты, разделяемый каждые четыре цифры, является наиболее распространенным формат, есть карточки с 15 цифрами (формат AMX XXXX XXXXXX XXXXX) и другие с 13 цифрами или даже с 19 цифрами (https://en.wikipedia.org/wiki/Payment_card_number ). Другая важная вещь, которую вы должны рассмотреть, - это настроить текстовый экран, чтобы разрешить только цифры, настроить тип клавиатуры, поскольку numberPad - хороший старт, но удобно реализовать метод, который защищает ввод.

Отправной точкой является выбор, когда вы хотите отформатировать номер, пока пользователь вводит номер или когда пользователь покидает текстовое поле. В случае, если вы хотите отформатировать, когда пользователь покидает текстовый экран, удобно используйте метод textFieldDidEndEditing (_:) делегировать содержимое текстового поля и отформатировать его.

В случае, когда вы, когда пользователь вводит номер, полезно textField (_: shouldChangeCharactersIn: replacementString:) метод делегата, который вызывается при изменении текущего текста.

В обоих случаях все еще есть проблема, выясните, какой правильный формат для введенного числа, IMHO и на основе всех чисел, которые я видел, есть только два основных формата: формат Amex с 15 цифрами, описанными выше, и формат, который содержит номер группы карт каждые четыре цифры, которые не заботятся о том, сколько цифр там, в данном случае, как правило, например, карта с 13 цифрами будет отформатирована XXXXX XXXX XXXX X и будет выглядеть 19 цифр например XXXX XXXX XXXX XXXX XXX, это будет работать для наиболее распространенных случаев (16 цифр) и для других. Таким образом, вы могли бы выяснить, как управлять случаем AmEx с тем же алгоритмом, что и при игре с магическими числами.

Я использовал RegEx, чтобы гарантировать, что 15-значная карта - это американский экспресс, в случае других конкретных форматов.

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

Я настоятельно рекомендую использовать специальный RegEx, который полезен для идентификации Эмитента и определения количества принятых цифр.

Теперь мой быстрый подход к решению с 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
}

и для shouldChangeCharactersIn: replacementString: Swift 3.0 От Jayesh Miruliya Ответ, поставьте разделитель между группой из четырех символов

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

Ответ 12

Swift 3.2

Небольшая коррекция в ответе и рабочем коде @Lucas в быстром 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
}

Ответ 13

Решение Swift 3 на основе Mark Amery Objective-C решение:

  • Внедрить действия и методы делегирования:

    textField.addTarget(self, action: #selector(reformatAsCardNumber(_:))
    textField.delegate = self
    
  • Методы делегата TextField и другие методы:

    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
    }
    

Ответ 14

Вот быстрая копия принятого ответа на случай, если кому-то это понадобится. Это в основном класс-оболочка. Я не потратил слишком много времени на его оптимизацию, но он готов к использованию.

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

Ответ 15

Вот ответ Котлина, основанный на Марке Эмери

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

Ответ 16

Отметьте это решение. Я нашел в Autorize.net SDK Пример.

Сделайте свой UITextField тип клавиатуры для Numeric.

Он будет маскировать номера кредитных карт с помощью "X", а путем добавления пробелов он сделает формат 'XXXX XXXX XXXX 1234'.

В файле заголовка .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;
    }
}

Ответ 17

Эти ответы - всего лишь слишком большой код для меня. Здесь решение в 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)
}

Ответ 18

Пожалуйста, используйте простую форму кредитной карты /**    См. Пример использования:    ### let str = "41111111111111111"

 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
}

Я надеюсь, что это будет полезно для вас.

Ответ 19

Пожалуйста, проверьте следующее решение, его работа отлично подходит для меня -

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
    }

Ответ 20

i изменен @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
}

Ответ 21

здесь есть модификация ответа от @sleeping_giant для быстрого. Это решение форматирует текст в формате "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
}

Ответ 22

Найден GIST в Github, который делает именно то, что мне нужно в Swift3 (https://gist.github.com/nunogoncalves/6a8b4b21f4f69e0fc050190df96a1e56)

Реализовано путем выполнения →

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

      if validator == true {

       } else {

       }
   }

Прекрасно работает в APP, который я разрабатываю, используя кредитные карты.

Ответ 23

в моем случае нам нужно сформировать число 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 метод, как показано ниже. Наш ибан сформировал начало 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;
}

Ответ 24

Создайте новый файл swift и вставьте ниже код, измените класс текстового поля на 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 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 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 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
    }
}

Больше использования будет найдено по ссылке ниже

Спасибо парню, который предоставил отличное решение для форматирования текста в UITextField.

http://vojtastavik.com/2015/03/29/real-time-formatting-in-uitextfield-swift-basics/

https://github.com/VojtaStavik/VSTextField

Работает отлично для меня.

Ответ 25

Swift 5, Xcode 10.2

Перепробовав много решений, я столкнулся с такими проблемами, как установка правильного положения курсора и форматирование в соответствии с потребностями, и, наконец, нашел решение после объединения двух сообщений (fooobar.com/info/114624/..., fooobar.com/info/114624/...)

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