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。

發佈了61 篇原創文章 · 獲贊 167 · 訪問量 37萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章