Swift 在用户输入时格式化文本字段

Swift format text field when user is typing

我正在尝试设置到期字段的格式,使其文本格式如下:MM / YY 当用户输入时,我可以让文本字段添加额外的字符,但是当我要删除数字时,代码将不允许您在再次添加“/”之前传递两个字符。有什么方法可以识别用户何时删除并绕过文本字段检查?

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!) / "
        }

    }
}

谢谢

您可以子class UITextField 并创建自定义字段以允许用户输入数字,只需将目标添加到您的对象以使用 select 或 select 或更新 UI.

首先让子class 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()
    }
}

我们还需要通过过滤所有非数字字符将它们转换为 Int 来正确格式化字段文本,在它们的字符串表示上使用 compactMap 并 returning 一个范围从 0 到 9 的 Int 数组。我们需要通过切换字符串中的位数来根据用户输入的位数放置斜线字符。考虑到它是一个过期字段,您还需要检查用户输入的月份和年份是否仍然有效。因此,让我们将月份和年份属性添加到 ExpirationField 以 return 它们的值。这同样适用于日期,因此我们可以将其与当前月份和年份进行比较以验证到期日期:


extension ExpirationField {
    var string : String { text ?? "" }
    var numbers: [Int]  { string.compactMap(\.wholeNumberValue) }
    var year:    Int    { numbers.suffix(2).integer }
    var month:   Int    { numbers.prefix(2).integer }
    @objc func editingChanged() {
        text = self.expirationFormatted
        if text?.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.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 { Calendar.iso8601.component(.year, from: self) }
    var month: Int { Calendar.iso8601.component(.month, from: self) }
}

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

然后您只需将一个文本字段拖到您的视图中,select 它并在检查器中将自定义 class 设置为 ExpirationField:

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

问题是您实施了错误的方法。实现委托方法 text​Field(_:​should​Change​Characters​In:​replacement​String:​)。这使您可以区分 哪里 文本正在更改(第二个参数是范围),以及 什么 新文本是 - 在这种情况下退格键,replacementString 将为空。