Formatting a UITextField for credit card input like (xxxx xxxx xxxx xxxx)

前端 未结 28 2081
长情又很酷
长情又很酷 2020-11-28 01:19

I want to format a UITextField for entering a credit card number into such that it only allows digits to be entered and automatically inserts spaces so that the

相关标签:
28条回答
  • 2020-11-28 01:41

    Below is a working Swift 4 port of Logicopolis's answer (which is in turn a Swift 2 port of an old version of my accepted answer in Objective-C) enhanced with cnotethegr8 's trick for supporting Amex cards and then further enhanced to support more card formats. I suggest looking over the accepted answer if you haven't already, since it helps explain the motivation behind a lot of this code.

    Note that the minimal series of steps needed to see this in action is:

    1. Create a new Single View App in Swift.
    2. On Main.storyboard, add a Text Field.
    3. Make the ViewController the delegate of the Text Field.
    4. Paste the code below into ViewController.swift.
    5. Connect the IBOutlet to the Text Field.
    6. Run your app and type in the 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
        }
    }
    

    Adapting this to other situations - like your delegate not being a ViewController - is left as an exercise for the reader.

    0 讨论(0)
  • 2020-11-28 01:41

    In order to achieve the goal of format the text entered in the textfield in this way XXXX XXXX XXXX XXXX is important to keep in mind some important things. Beside the fact that the 16 digits card number separated every four digit is the most common used format, there are cards with 15 digits (AmEx formatted XXXX XXXXXX XXXXX) and others with 13 digits or even with 19 digits (https://en.wikipedia.org/wiki/Payment_card_number ). Other important thing you should consider is configure the textField to allow only digits, configure the keyboard type as numberPad is a good start, but is convenient to implement a method which secure the input.

    A starting point is decide when you want to format the number, while the user is entering the number or when the user leave the text field. In the case that you want to format when the user leave the textField is convenient to use the textFieldDidEndEditing(_:) delegate's method take the content of the textField and format it.

    In the case you while the user is entering the number is useful the textField(_:shouldChangeCharactersIn:replacementString:) delegate method which is called whenever the current text changes.

    In both cases there is still a problem, figure out which is the correct format for the entered number, IMHO and based on all the numbers that I have seen, there are only two main formats: the Amex format with 15 digits described above and the format which group card number every four digits which don not care of how much digits there are, being this case like a generic rule, for example a card with 13 digits will be formatted XXXXX XXXX XXXX X and with 19 digits will look like this XXXX XXXX XXXX XXXX XXX, this will work for the most common cases (16 digits) and for the others as well. So you could figure out how to manage the AmEx case with the same algorithm below playing with the magic numbers.

    I used a RegEx to ensure that a 15 digits card is an American express, in the case of other particular formats

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

    I strongly recommend to use the specific RegEx which is useful to identify the Issuer and to figure out how many digits should be accepted.

    Now my swift approach of the solution with textFieldDidEndEditing is

    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
    }
    

    and for shouldChangeCharactersIn:replacementString: a Swift 3.0 From Jayesh Miruliya Answer, put a separator between the group of four characters

     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
            }
        }
    
    0 讨论(0)
  • 2020-11-28 01:41

    here is the modification of the answer from @sleeping_giant for swift. This solution formats the text in 'xxxx-xxxx-xxxx-xxxx-xxxx' format and stops accepting any numbers beyond that range.

    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
    }
    
    0 讨论(0)
  • 2020-11-28 01:43

    You can use my simple library: DECardNumberFormatter

    Example:

    // You can use it like default UITextField
    let textField = DECardNumberTextField()
    // Custom required setup
    textField.setup()
    

    Output:

    For sample card number (Visa) 4111111111111111
    Format (4-4-4-4): 4111 1111 1111 1111
    
    For sample card number (AmEx) 341212345612345
    Format (4-6-5): 3412 123456 12345
    
    0 讨论(0)
  • 2020-11-28 01:44

    I think this one is good:

    -(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;
        }
    
    0 讨论(0)
  • 2020-11-28 01:46

    Define below method & call it in UITextfield delegates or wherever required

    -(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;
    }
    
    0 讨论(0)
提交回复
热议问题