Swift format text field when user is typing

2019-03-05 04:44发布

I'm trying to format an expiry field so that it formats the text as follows: MM / YY I can get the textfield to add in the extra characters when the user is typing however when I come to delete the numbers the code will not allow you to go passed two characters before adding in the " / " again. Is there a way I can recognise when the user is deleting and bypass the text field check?

ExpiryOutlet.addTarget(self, action: #selector(ExpiryDidChange(_:)), for: .editingChanged)

func ExpiryDidChange(_ textField: UITextField) {
    if textField == ExpiryOutlet {

        if textField.text != "" && textField.text?.characters.count == 2 {
            textField.text = "\(textField.text!) / "
        }

    }
}

Thanks

2条回答
家丑人穷心不美
2楼-- · 2019-03-05 05:15

You can subclass UITextField and create a custom field to allow the user to input numbers only by adding a target to your object for controlEvents editingChanged with a selector to update UI.

First lets subclass UITextField:

class ExpirationField: UITextField {
    var allowsExpiredDate = false
    override func didMoveToSuperview() {
        super.didMoveToSuperview()
        placeholder = "MM/YY"
        addTarget(self, action: #selector(editingChanged), for: .editingChanged)
        keyboardType = .numberPad
        textAlignment = .center
        editingChanged()
    }
}

We need also to properly format the field text by filtering all non digits characters converting them to Int using flatMap on the string representation of them and returning an array of Int ranging from 0 to 9. We need to place the slash character depending on the numbers of digits entered by the user by switching the number of digits in the string. Considering the fact that it is an expiration field you will need also to check if the month and year entered by the user still valid. So lets add month and year properties to ExpirationField to return their value. The same applies to the Date so we can compare it to the current month and year to validate the expiration date:


extension ExpirationField {
    var string : String { return text ?? "" }
    var numbers: [Int]  { return string.characters.flatMap{ Int(String($0)) } }
    var year:    Int    { return numbers.suffix(2).integer }
    var month:   Int    { return numbers.prefix(2).integer }
    func editingChanged() {
        text = expirationFormatted
        if text?.characters.count == 5 {
            print("Month:", month, "Year:", year, "isValid:", isValid)
            if !allowsExpiredDate && !isValid {
                text = numbers.prefix(2).string + "/" + numbers.dropLast().suffix(1).string
            }
        } else {
            print("isValid:", false)
            switch numbers.count {
            case 1 where numbers.integer > 1:
                text = ""
            case 2 :
                if numbers.integer > 12 {
                    text = "1"
                } else if numbers.integer == 0 {
                    text = "0"
                }
            case 3 where (numbers.last ?? 0) < 1 && !allowsExpiredDate:
                text = numbers.dropLast().string
            case 4 where year + 2000 < Date().year && !allowsExpiredDate:
                text = numbers.prefix(2).string + "/" + numbers.dropLast().suffix(1).string
            default:
                break
            }
        }
        if isValid {
            layer.borderColor = UIColor.darkGray.cgColor
            layer.cornerRadius = 3
            layer.borderWidth = 1
        } else {
            layer.borderColor = UIColor.clear.cgColor
            layer.borderWidth = 0
        }
    }
    var expirationFormatted: String {
        let numbers = self.numbers.prefix(4)
        switch numbers.count {
        case 1...2: return numbers.string
        case 3: return numbers.prefix(2).string + "/" + numbers.suffix(1).string
        case 4: return numbers.prefix(2).string + "/" + numbers.suffix(2).string
        default: return ""
        }
    }
    var isValid: Bool {
        if string.characters.count < 5 { return false }
        guard 1...12 ~= month  else {
            print("invalid month:", month)
            return false
        }
        guard Date().year-2000...99 ~= year else {
            print("invalid year:", year)
            return false
        }
        return year > Date().year-2000 ? true : month >= Date().month
    }
    override func deleteBackward() {
        text = numbers.dropLast().string
        text = expirationFormatted
        layer.borderColor = UIColor.clear.cgColor
        layer.borderWidth = 0
    }
}

extension Calendar {
    static let iso8601 = Calendar(identifier: .iso8601)
}

extension Date {
    var year: Int {
        return Calendar.iso8601.component(.year, from: self)
    }
    var month: Int {
        return Calendar.iso8601.component(.month, from: self)
    }
}

extension Collection where Iterator.Element == Int {
    var string: String {
        return map(String.init).joined()
    }
    var integer: Int { return reduce(0){ 10 * $0 + $1 } }
}

Then You just drag a text field to your view, select it and set custom class to ExpirationField in the inspector:

enter image description here

Sample

查看更多
ら.Afraid
3楼-- · 2019-03-05 05:28

Is there a way I can recognise when the user is deleting and bypass the text field check?

The problem is that you've implemented the wrong method. Implement the delegate method text​Field(_:​should​Change​Characters​In:​replacement​String:​). This allows you distinguish where the text is changing (the second parameter is the range), and what the new text is — in the case of the backspace, replacementString will be empty.

查看更多
登录 后发表回答