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

前端 未结 28 2172
长情又很酷
长情又很酷 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.. 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.

提交回复
热议问题