如何对用户输入进行校验

Posted 颐和园

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何对用户输入进行校验相关的知识,希望对你有一定的参考价值。

对用户输入进行校验是非常重要,我们无法预知用户行为。当你要求用户输入信息时,用户很可能输入了不正确的格式,而一旦我们将这些错误格式的数据发送到 API,往往会导致不可预期的后果,甚至程序崩溃。因此理论上只要是用户输入,我们都必须进行校验。

但这不是一件简单的工作。我们不仅仅需要考虑正则校验,同时需要遵循响应式原则,尽早在用户输入的过程中发现一切错误,越早越好。仅仅在用户输入完成后才进行正则校验,不是一种良好体验。

打开我们的示例工程,build& run,你可以真实地看一下一个良好体验的用户输入校验是什么样子:

  • 首先,它是响应式的,用户每输入一个字符都会立即得到校验并在 UI 上体现。
  • 其次,丰富的 UI 表现,给予用户良好的视觉反馈。无论检测到什么错误,都会以多种方式加以展现,比如文字颜色变化、下划线样式变化、按钮的失效/生效,以及多行的文字提示。
  • 禁止无效输入。当用户输入的字符达到最大长度限制,不会允许用户继续输入。

接下来,我们演示这将如何实现。

NameFieldValidator

假设我们将对用户名进行校验,我们用一个单独的类实现这个逻辑。新建 NameFieldValidator.swift,首先定义一个枚举类型如下:

import UIKit

public enum ValidationResult: Equatable 
    case error(Bool, Bool, Bool) // under characters limit, beyond characters limit, has special character
    case empty
    case valid
    case badword
    case blank

该枚举用于代表校验结果。结果可能有几种:

  • error(Bool, Bool, Bool) 用户输入的内容中存在错误,这又可能包含 3 种情况:字符串太长,超出最大长度限制;字符串太短,小于最小长度限制;字符串中包含不被允许的字符。注意3 种情况可能存在1种,也可能兼而有之,我们用 3 个关联类型(Bool)表示它们。
  • emtpy,用户根本没有输入。
  • valid,用户输入有效。
  • badword,用户输入了禁止使用的词语(黑名单)。这需要后端校验。前端只是根据后端校验结果进行 UI 展示。
  • blank,用户输入的字符是不可见字符,比如一个或多个空格。

接下来定义属性:

    var maxCharacters = 13
    var minCharacters = 2
    var textRed = UIColor.hexColor(hex: "d23939")
    var state: ValidationResult = .empty 
        didSet 
            updateUI(validationResult: state)
        
    

分别是:最大字符限制、最小字符限制、错误发生时 text field 的文字颜色(偏红色),校验结果。

其中 state 带有一个属性监视器,一旦 state 被改变,将自动调用 updateUI 方法。

以及几个弱引用属性:

    weak var bottomLine: UIView?
    weak var nextButton: UIButton?
    weak var errorDisplayLabel: UILabel?
    weak var nameTextField: UITextField?

这些属性分别引用 View Controller 中的一些 UI 组件,因为我们会根据校验的结果刷新这些组件。它们分别是:

  • nameTextField,文本框,用户输入的地方。
  • bottomLine,一条黑细线,位于文本框的下方。
  • errorDisplayLabel,一个 Label,位于文本框下方,用于显示不同的错误信息。
  • nextButton,一个按钮,用于确认/提交用户输入的数据。

然后是 init 方法,用于将上面 4 个属性实例化:

    public init(nameTextField:UITextField, bottomLine: UIView, errorDisplayLabel: UILabel, nextButton: UIButton) 
        super.init()
        self.nameTextField = nameTextField
        nameTextField.text = nil // Instead of setting textfield's text property directly, we can use setTextInitially method to set textfield's text.
        self.bottomLine = bottomLine
        self.errorDisplayLabel = errorDisplayLabel
        self.nextButton = nextButton
        nameTextField.delegate = self
    

同时将 text field 的 delegate 指向自身,这样需要 NameFieldValidator 实现 UITextFieldDelegate 协议,也就是 textField(_, shouldChangeCharactersIn:, replacementString: )方法:

extension NameTextFieldValidator: UITextFieldDelegate   
    public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool 
        let text = textField.text ?? ""
        var newText = text
        if let range = Range(range, in: text) 
            newText = text.replacingCharacters(in: range, with: string)
        
        if string.isEmpty  //  In swift 5.3, when the user deletes one or more characters, the replacement string is empty
            nameTextField?.deleteBackward()
        else if newText.count <= maxCharacters 
            textField.text = newText
        
        testInputString(newText)
        return false
    

这个方法拦截 text field 的输入事件。每输入一个都会调用此方法。首先这个方法我们只会返回 false,这导致无论用户输入什么,我们都不会采用用户的输入作为 text field 的最终值。这样的好处在于 text field 的输入将由我们完全掌控。

首先,我们计算出 newText,也就是用户这次输入将可能最终导致 text field 的 text 变成什么样子。但这只是有可能,因为最终是否采用要根据我们的校验而定。

然后,判断用户按键是否是 delete 键,这只需要判断 replacementString 是否为空即可,这是 swift 5.3 的新特性,不然要判断 delete 键在以前是一件相当麻烦的事情。如果是 delete 键,直接调用 text field 的 deleteBackward() 方法将当前光标回删一格,这是系统默认的行为,所以这里返回 true 也是可以的。

如果不是 delete 键,那么我们要判断 newText 的长度是否超过了最大限制,只有小于最大限制,我们才会改变 text field 的 text 值。

最后调用我们的特殊逻辑 testInputString 方法,对用户当前输入进行校验。注意,这里使用的是 newText,而非 text field 的 text。二者有何区别?

text field 的 text 是 text field 显示在屏幕上的文本内容,而 newText 是当前正在输入的值,它有可能等于 text field 的 text ——如果它没有长度限制,我们会直接将它赋值给 text field 的 text。也有可能不等于 text field 的 text ——如果超长的话,我们不会进行赋值,此时 text field 的值将保持上一次输入时的值,本次输入被舍弃。

接下来看一下 testInputString 方法的实现:

    func testInputString(_ input: String) 
        state = validate(string: input)
        let realResult = validate(string: nameTextField?.text)
        nextButton?.isEnabled = realResult == .valid
    

validate 方法验证一个字符串是否有效,返回结果时一个 ValidateResult 枚举。然后用 updateUI 去更新 UI(这里之前定义的弱引用属性中的 3 个将派上用场)。但是 nextButton 需要一些特别的处理,因为它的状态是根据 text field 的 text 值而定的,而非前面的 newText。所以实际上我们要校验两次,一次是 newText,更新除 nextButton 之外的 UI,一次是 text field 的 text,更新 nextButton 的 UI。

updateUI 负责更新除 nextButton 之外的 UI:

    public func updateUI(validationResult: ValidationResult)
        let red = textRed
        switch validationResult 
        case .error(let underMin, let exceedLen, let specialChar):
            nameTextField?.textColor = red
            bottomLine?.backgroundColor = red
            var message = ""
            if exceedLen 
                message.append("exceeds max length limit")
                message.append("\\n")
            else if underMin 
                message.append(String(format: "less than min lenghth limit: %@ characters", "\\(minCharacters)"))
                message.append("\\n")
            
            if specialChar
                message.append("exists unallowed characters")
            
            errorDisplayLabel?.text = message
            errorDisplayLabel?.isHidden = false
        case .badword:
            errorDisplayLabel?.text = "can not be special words"
            errorDisplayLabel?.isHidden = false
            bottomLine?.backgroundColor = red
            nameTextField?.textColor = red
            nameTextField?.text = nameTextField?.text
        case .blank:
            errorDisplayLabel?.text = "can not be blank"
            errorDisplayLabel?.isHidden = false
            bottomLine?.backgroundColor = red
        default:
            errorDisplayLabel?.text = nil
            nameTextField?.textColor = .black
            nameTextField?.text = nameTextField?.text
            nextButton?.isEnabled = validationResult == .valid
            bottomLine?.backgroundColor = UIColor.hexColor(hex: "c5c5c5")
        
    

内容虽然有点多,但不难理解,就是根据对 newText 的校验结果进行不同的展示。

核心的内容还是 validate 方法,它对字符串进行校验:

    func validate(string input: String? ) -> ValidationResult 
        guard let input = input, !input.isEmpty else
            return .empty
        
        guard !input.isBlank else
            return .blank
        
        let exceedMaxLength = input.count > maxCharacters
        let underMinLength = input.count < minCharacters

        var validatedString = input
        if exceedMaxLength  // if input's length exceed limit, trim it to limit.
            validatedString = String(input[...(maxCharacters-1)])
        
        let hasSpecialCharacter = !validatedString.isValidFirstName
        
        guard !exceedMaxLength && !hasSpecialCharacter && !underMinLength else 
            return .error(underMinLength,exceedMaxLength, hasSpecialCharacter)
        
        return .valid
    

首先对空串和空白字符进行检查,然后对最长字符限制和最短字符限制进行检查,最后进行正则校验,看有没有特殊字符。如果都没有,返回 .valid。

正则校验使用的是 String 扩展:

extension String 
    // MARK: regex expression
    // * any Letters and Numbers (in any language) are allowed
    // * the following characters are allowed: ; !, ?, &, /, -, _, ', #, ., ,,
    private static let firstName = "^[\\\\pL\\\\pN!\\\\?&\\\\/ _'#\\\\.,;-]+$"
    
    
    // Determine if string is a valid first name.
    var isValidFirstName: Bool 
        let predicate = NSPredicate(format: "SELF MATCHES %@", type(of:self).firstName)
        return predicate.evaluate(with: self)
    

就是正常的的正则校验而已,没有特别的地方。

使用 NameFieldValidator

我们通过故事板来使用 NameFieldValidator。打开 main.storyboard,你可以看到画布中包含了4 个 UI 组件:

它们分别链接到 ViewController.swift 中的几个 IBOutlet:

    @IBOutlet weak var continueButton: UIButton!
    @IBOutlet weak var bottomLine: UIView!
    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var errorDisplayLabel: UILabel!

在 viewDidLoad 方法中:

    private var nameValidator: NameTextFieldValidator?
    override func viewDidLoad() 
        super.viewDidLoad()
        
        nameValidator = NameTextFieldValidator(nameTextField: textField, bottomLine: bottomLine, errorDisplayLabel: errorDisplayLabel, nextButton: continueButton)
        nameValidator?.setTextInitially("your name")
        nameValidator?.maxCharacters = 10
        nameValidator?.minCharacters = 4
    

setTextInitially 方法调用一句有点特别,因为我们需要考虑 textField.text 可能存在默认字符串值的情况,那样当 app 一启动时有可能 textField 中会存在非法的字符串,而我们的 NameFieldValidator 却没有任何错误提示。因此我们用 setTextInitially 方法替代直接设置 textField.text 的值,这样保证 text field 中出现的内容都是经过校验的:

    func setTextInitially(_ string: String?) 
        if let name = string 
            nameTextField?.text = name
            self.testInputString(name)
        else 
            nextButton?.isEnabled = !string.isNilOrBlank
        
    

badword

关于 badword,这需要 API 配合,出于演示目的,我们用延迟 1 秒来模拟一个 API 异步调用:

fileprivate func checkBadword(name: String?, callback: @escaping (Bool)->()) 
        // simulate network request
        DispatchQueue.main.asyncAfter(deadline: .now()+1) 
            callback(name == "fool")
        
    

当 API 检测到提交的字符串等于 fool 时,返回 true,表示这是一个 badword,否则返回 false。然后在故事版中为 continueButton 创建一个 IBAction 链接并实现方法代码:

    @IBAction func continueAction(_ sender: Any) 
        checkBadword(name: textField.text)  [weak self] isBadword in
            if isBadword 
                self?.nameValidator?.state = .badword
            
        
    

我们检测 API 返回的 Bool 值,如果是 badword,我们将 NameValidator 状态改为 .badword。

更多 init

在一些简单场景中,我们的 UI 未必有多复杂,可能就一个 UITextField 和一个 UIButton 足矣,这种情况下 NameFieldValidator 仍然可以使用,你可以调用两个参数的 init 方法:

    public init(nameTextField:UITextField, nextButton: UIButton) 
        super.init()
        self.nameTextField = nameTextField
        self.nextButton = nextButton
        nameTextField.delegate = self
    

其它缺失的弱引用属性将自动置为 nil。这样当校验器的状态发生改变,只有 text field 和 nextButton 的 UI 会得到更新。最简单的init版本,是只有一个参数的版本:

    public init(nameTextField:UITextField) 
        super.init()
        self.nameTextField = nameTextField
        nameTextField.delegate = self
    

复用性

NameFieldValidator 与 UI 绑定的特性,使得它的复用性非常一般。但通过对本教程的学习,你可以在此基础上轻易扩展出自己的输入校验类。

以上是关于如何对用户输入进行校验的主要内容,如果未能解决你的问题,请参考以下文章

如何对用户输入进行校验

全方位的Django

Checksum 一个良好的校验和算法通常会对进行很小的修改的输入数据都会输出一个显著不同的值

Django——forms 组件

Dubbo服务如何优雅的校验参数

Form和ModelForm