【问题标题】:Swift format text field when user is typing用户键入时的 Swift 格式文本字段
【发布时间】:2017-03-25 09:34:02
【问题描述】:

我正在尝试格式化过期字段,以便它按如下方式格式化文本: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!) / "
        }

    }
}

谢谢

【问题讨论】:

  • 你总是可以有两个UITextField,中间有一个显示“/”的标签,并使用相同的逻辑来切换firstResponder状态

标签: swift uitextfield uitextfielddelegate


【解决方案1】:

您可以继承 UITextField 并创建一个自定义字段,以允许用户仅通过向您的对象添加一个目标来输入数字,以便使用选择器来更新 UI 的 controlEvents editingChanged。

首先让 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 并返回一个从 0 到 9 的 Int 数组。我们需要根据情况放置斜杠字符通过切换字符串中的位数来确定用户输入的位数。考虑到它是一个过期字段,您还需要检查用户输入的月份和年份是否仍然有效。因此,让我们将月份和年份属性添加到 ExpirationField 以返回它们的值。这同样适用于日期,因此我们可以将其与当前月份和年份进行比较以验证到期日期:


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 * $0 + $1 } }
}

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

【讨论】:

  • 子类的意义何在? editingChanged 只需要一个行动目标。
  • 我没有说它有什么问题。但这似乎完全没有必要,那为什么要这样做呢?
  • 这对于我所追求的来说看起来有点矫枉过正。但是很棒的东西!
  • 感谢您提供出色的解决方案,但我需要更改格式化程序 MM/YYYY 并且在 2000-2018 之间有限制如何编辑它? @LeoDabus
【解决方案2】:

有没有一种方法可以识别用户何时删除并绕过文本字段检查?

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

【讨论】:

  • 是的,这解决了这个问题,同时识别出用户从文本字段中删除了一个数字。
猜你喜欢
  • 2015-03-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-11
  • 1970-01-01
  • 1970-01-01
  • 2019-02-16
相关资源
最近更新 更多