Swift format text field when user is typing

落爺英雄遲暮 提交于 2019-12-02 03:33:49

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.

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:

Sample

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!