codecamp

MBTextFieldWithInputValidator 诞生记

前言

MBTextFieldWithInputValidator 这个库其实 4 个月以前就已经用 Swift2.0 实现并上传到了我的github,之后一直想写篇博客分享一下我写这个库时的一些思路,但是因为各种事情,一直到今天才开始写这篇博客。当然目前的设计肯定也不是那么完美,如果有好的建议,请直接在下方评论回复,qqq~

简介

功能

实现对UITextField输入内容进行验证的功能,如果内容不符合验证策略,则弹出错误信息提示用户

背景

原来项目中使用的验证器和 UITextField 控件本身是分离的,设计的思路如下:

建一个验证器类,然后为每种验证策略(如:手机,密码等)提供一个验证方法,传入参数为 UITextField 的内容,验证通过返回 true,否则返回 false

而验证的步骤如下:

  1. 在提交表单之前拿到各个 UITextField 中的内容。
  2. 为每个 UITextField 中的内容调用相应的验证方法来进行验证。
  3. 如果有验证不通过,就不能提交表单,在提交表单的方法中弹出相应的告警信息。

这种方式有如下几个硬伤:

  1. 如果需要增加一种验证策略,则需要修改验证器类的代码,后期验证策略变多,验证器类的代码会变得相当臃肿,不易维护。
  2. 在提交表单时进行验证,就会出现很多 if-else 分支语句,也就是我们常说的“鞭尸金字塔“。可能验证就占据了提交表单方法的一大半,很影响代码的阅读。
  3. 在提交表单的方法中弹出相应的告警信息,其实各种验证规则的提示大同小异,并不需要每次都写。

在这种情况下,我们就需要设计另外一种更优雅的验证方式,当时部门总监提供了设计思路的一个雏形,然后由我进行了 OC 的实现,第一版只能给每个 UITextField 提供一种验证(比如验证是否手机),后来我用 Swift 实现了第二版,实现了为每个 UITextField 提供多种验证的功能(比如可以先验证是否为空,再验证是否手机)。

下面是具体的设计思路。

思路

结构图

结构图

主要模块及功能如下:

  • MBInputValidator:验证器基类,负责提供统一的接口供 MBTextFieldWithInputValidator 调用,验证器子类通过继承并重写其验证策略方法即可以实现具体的验证策略。
  • MBTextFieldWithInputValidatorUITextField 子类,负责提供统一的外部接口供业务代码调用以及验证出错时弹出告警信息。

模块详细介绍

MBInputValidator

验证器基类,因为要实现顺序的多个验证的功能,所以它需要包含指向下一个验证器的属性:

@IBOutlet var next:MBInputValidator?

定义为可选值的原因是最后一个验证器的 nextnil

有了 next,我们就需要一个特殊的构造器:

convenience init(next:MBInputValidator?) {
    self.init()
    self.next = next
}

这个构造器会在后面设置 UITextField 控件验证器的时候使用。

它还包含一个错误描述的内部类:

class ErrorDesc {
    init(title:String, leading:String, trailing:String) {
        self.title = title
        self.leading = leading
        self.trailing = trailing
    }
    var title:String? //错误信息的标题(如:温馨提示)
    var leading:String? //错误信息的前缀,用来拼接到输入控件名字的头部(请输入手机号)
    var trailing:String? //错误信息的后缀,用来拼接到输入控件名字的尾部(输入控件名字是:密码,trailing 是:须由6-12位的字母和数字组成,错误信息就是:密码须由6-12位的字母和数字组成)
}

然后有一个供 MBTextFieldWithInputValidator 调用的统一接口:

func validateInput(input:UITextField) -> ErrorDesc?{
    return nil
}

这个方法传入 UITextField 控件 input,然后返回 ErrorDesc 错误描述信息,验证器子类就是重写这个方法来实现具体的验证策略,来看一个密码的验证策略:

override func validateInput(input:UITextField) -> ErrorDesc?{
    if false == super.validateInput(input, regexString: "^[A-Za-z0-9]{6,12}$") {
        return ErrorDesc(title: "温馨提示", leading: "", trailing: "须由6-12位的字母和数字组成")
    }
    return nil
}

这里面有调用了 super 的另外一个 validateInput 方法,内容如下:

func validateInput(input:UITextField, regexString:String?) -> Bool{
    if nil == regexString { // 如果正则表达式为空做非空判断
        // 首先做去空格处理
        let trim = input.text?.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
        return 0 != trim?.lengthOfBytesUsingEncoding(NSUTF8StringEncoding)
    }else { // 否则做正则表达式判断
        do {
            let regex:NSRegularExpression =  try NSRegularExpression(pattern: regexString!, options: NSRegularExpressionOptions.AnchorsMatchLines)
            let numberOfMactches = regex.numberOfMatchesInString(input.text!, options: NSMatchingOptions.Anchored, range: NSMakeRange(0, (input.text?.lengthOfBytesUsingEncoding(NSUTF8StringEncoding))!))
            if 0 == numberOfMactches {
                return false
            }
        } catch {
            return false
        }
        return true
    }
}

这个方法这个方法传入 UITextField 控件 input 和正则表达式串 regexString,它负责具体的验证过程,验证成功返回 true,否则返回false,采用正则表达式匹配的方式是目前主流的做法。

MBTextFieldWithInputValidator

UITextField 子类,使用时需要将 UITextField 控件的类型指定为:MBTextFieldWithInputValidator这是目前这个库中已知的不足点,子类的方式侵入性太强,后期会用 extension 的方式重新实现)。它包含了指向第一个验证器的属性:

@IBOutlet var inputValidator:MBInputValidator?

然后有一个供业务功能调用的方法:

func validate(inputName:String, shouldAlert:Bool) -> MBInputValidator.ErrorDesc? {
    // 调用另外一个私有的 validate 方法 
    let error = self.validate(self.inputValidator)
    if nil != error {
        let errorReason = (error?.leading)!+inputName+(error?.trailing)!
        if true == shouldAlert { // 如果需要显示错误信息就弹出错误对话框
            self.showAlertMessage((error?.title)!, message: errorReason)
        }
    }
    return error
}

这个方法传入控件名 inputName 和 是否弹窗告警 shouldAlert,然后返回 ErrorDesc 错误描述信息,从而也可以让业务自己去做错误显示处理。

在上面的代码中有调用一个私有的 validate 方法,其内容如下:

private func validate(validator:MBInputValidator?) -> MBInputValidator.ErrorDesc?{
    if nil == validator {
        return nil
    }
    let ret = self.validate(validator!.next)
    if nil != ret {
        return ret
    }
    return validator!.validateInput(self)
}

这个方法是实现链式验证的关键,之前已经说过,每个验证器都包含指向下一个验证器的属性,这样所有的验证器就是以链表的方式连接在一起。所以我们以递归的方式一直到拿到最后一个验证器,然后在递归栈 pop 的时候调用每个验证器的 validateInput 方法。因为是 pop 的时候调用,所以最后一个验证器的验证策略会最先做验证

使用方法

业务使用

phoneField.inputValidator = MBPhoneInputValidator(next:MBNumberInputValidator(next:MBEmptyInputValidator()))

上面的代码给 nickNameField 加了两项验证,先验证是否为空,再验证内容是否是数字,最后验证是否为手机。它验证器链的结构如下:

验证器链

然后在提交表单时调用下面的方法即可完成验证逻辑:

if nil != phoneField.validate(phoneField.placeholder!, shouldAlert: true) {
    return;
}

实现自己的验证器

只需要继承 MBInputValidator,然后重写其 func validateInput(input:UITextField) -> ErrorDesc? 即可。

这样就实现了验证策略和业务的分离,也便于后期扩展和维护。

想要进一步了解的,可以点击文章顶部的地址下载demo。

使用 RDVTabBarController 制作底部凸起的 TabBar 笔记
iOS代码规范(Swift 与 OC 混编版)
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }