[iOS] UITextField基礎和一些高級功能

本文翻譯自 https://grokswift.com/uitextfield/


在iOS中,Apple為我們提供了三種可以顯示和接收字符輸入的方式:UILabel, UITextField, UITextView, 什么時候該使用哪種方式有時候也會令人非常困惑.

如果你僅僅需要顯示一些文字而不需要輸入文字,那么需要使用UILabel, 有時候你可能會聽到使用UITextView來顯示特殊格式的文字的這種說法, 但是這已經過時了, 如今使用AttributedString你也可以做UITextView能做的大部分事情, 一般情況下應該首先嘗試使用UILabel, 之后如果真的需要再使用UITextView.

如果你需要接收用戶的輸入,那么你需要使用UItextField或者UITextView, 如果僅僅只有一行文字,你應該使用UItextField, 有多行文字的話, 應該使用UItextView.

我自己開發的APP一般有很多UILabel,有一些UITextField,同時只有很少的UITextView,現在我們來看看我們使用UITextField的需求,這些需求我們在開發過程中是一定會遇到的:

  • 限制輸入字符數量
  • 只允許輸入特定字符(或者不允許特定字符)
  • 保存輸入的內容并且在APP再次打開的時候還原內容
  • 點擊返回鍵收回鍵盤

我們同時也會接觸到一些UITextField內建的顯示方式和行為

建立項目

本案例基于Swift2.0和Xcode7.1

為了一起愉快地玩耍, 我們新建一個SingleViewApplication, 并且拖一個UITextField進去, 給它加上約束.如圖

約束
效果

UITextField 綁定一個屬性, 同時讓ViewController成為它的代理.

class ViewController: UIViewController, UITextFieldDelegate {
 @IBOutlet weak var textField: UITextField!
 ...
}
詳情

現在我們就已經準備好來大干一番了.

限制輸入的字符數量

使用UITextField的時候, 限制輸入的字符數量是一個十分普遍的要求, 你可以在UITextField的代理方法中實現這個要求.

func textField(textFieldToChange: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool

上面這個方法會在textField中輸入的字符發生改變的時候觸發, 它有三個參數:

  1. textFieldToChange: UITextField --哪個UITextField發生了改變
  2. shouldChangeCharactersInRange range: NSRange --發生改變的跨度(包含開始位置和長度),我們稍后會詳細闡述這個參數的作用
  3. replacementString string: String --增加的字符,如果是刪除字符的話, 這個值為空

textField的改變方式可能有很多種情況, 隨之以上三個參數有多種組合

  • 增加字符: range為空,replacementString的長度是1
  • 刪除字符: range是1(如果是剪切或者選擇了多個字符跨度會更大), replacementString為空
  • 在字符中粘貼(一次性增加多個字符): range為空(因為沒有字符被選擇), replacementString長度大于1
  • 清除全部字符(通過剪切操作或者點擊清除按鈕): range大于1個字符,replacementString為空
  • 通過粘貼或者輸入替換了選中的字符: range大于1, replacementString長度大于1

自動糾正功能和上面的粘貼類似: 可增加多個字符或者替換任意選中的字符

我們無法通過上面的方法來決定一個字符在發生改變后能否被顯示, 因為一個名為 shouldChangeCharactersInRange的方法會在字符改變之前被觸發,因此我們不能僅僅是在發生改變之后去檢查字符的變化.

為了限制輸入的字符的數量,我們不需要知道具體的字符是什么, 僅僅知道他們的長度就足矣.我們需要先計算改變之前的字符的長度, 再加上將要增加的字符的長度, 如果他們的和小于限制數量則放行, 否則就將超出的部分刪掉.

// 先計算出改變之后的字符串總長度
let startingLength = textFieldToChange.text?.characters.count ?? 0
let lengthToAdd = string.characters.count
let lengthToReplace = range.length
let newLength = startingLength + lengthToAdd - lengthToReplace

在Swift中我們需要通過字符來計算String的長度

let stringLength = myString.characters.count

我們使用空合運算來保證無法獲取原始字符串長度的時候將startingLength設置為 0 (關于空合運算, 大家可以參考文末的相關鏈接, 文章的作者只是很簡單的介紹,和主旨不符,不再翻譯)

將計算過程放在具體的代理方法中, 使用一個局部變量characterCountLimit來表示對字符數量的限制, 之后我們就可以計算出字符的改變是否超出范圍了.

func textField(textFieldToChange: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
  // 設置字符限制為4個字符
  let characterCountLimit = 4
  
  // 先計算出改變之后的字符串總長度
  let startingLength = textFieldToChange.text?.characters.count ?? 0
  let lengthToAdd = string.characters.count
  let lengthToReplace = range.length
  
  let newLength = startingLength + lengthToAdd - lengthToReplace
  
  return newLength <= characterCountLimit
}

當總長度小于或者等于設定的限制數目時會允許輸入,否則不會允許輸入.

現在我們可以運行這個項目并且進行測試看看是否有效果了, 如果是模擬器,還可以使用 CMD + CTRL + Z 來模擬搖晃設備產生撤銷功能.

禁止輸入某個字符

對輸入進textField的字符進行過濾和進行長度限制其實并沒有什么不同, 我們也是在字符完成輸入之前進行判斷輸入的有效性, 因此我們還是使用和上文相同的代理方法:

func textField(textFieldToChange: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool

假設我們的需求是不允許輸入標點符號, 那么我們可以通過NSCharacterSet來檢查輸入的字符中是否包含標點符號:

let characterSetNotAllowed = NSCharacterSet.punctuationCharacterSet()

如果你需要創建一個自定義的NSCharacterSet, 最簡單的方法是通過String來創建:

let characterSetAllowed = NSCharacterSet(charactersInString: "abcd")

檢查一個string是否包含一個NSCharacterSet中的元素, 我們使用rangeOfCharacterFromSet方法來實現:

let rangeOfCharacter = string.rangeOfCharacterFromSet(characterSetNotAllowed, options: .CaseInsensitiveSearch)

上面的rangeOfCharacter包含了characterSetNotAllowed這個NSCharacterSet中的某個元素第一次出現時的位置, 通過它我們可以做我們想做的了, 如果含有標點符號,我們在代理方法中返回false:

if let _ = string.rangeOfCharacterFromSet(characterSetNotAllowed, options: .CaseInsensitiveSearch) { 
  return false // they're trying to add not allowed character(s)
} else { 
  return true // all characters to add are allowed
}

最后整個代理方法就像這樣:

func textField(textFieldToChange: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
  let characterSetNotAllowed = NSCharacterSet.punctuationCharacterSet()
  if let _ = string.rangeOfCharacterFromSet(characterSetNotAllowed, options: .CaseInsensitiveSearch) {
    return false
  } else {
    return true
  }
}

現在保存文件并運行, 測試一下我們的代碼(我相信沒什么問題).

**注意: **
本方法只在用戶輸入的時候起作用, 通過代碼直接向textField填寫字符的時候是不起作用的.

只允許某些字符

如果情況變了, 我們希望能夠只允許某些特定的字符被輸入, 其他字符一律不準輸入, 怎么辦? 當然還是通過rangeOfCharacterFromSet啦, 我們只需要檢查所有輸入的字符都在characterSetAllowed中即可, 直接上代碼:

func textField(textFieldToChange: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
  let characterSetAllowed = NSCharacterSet.punctuationCharacterSet()
  if let rangeOfCharactersAllowed = string.rangeOfCharacterFromSet(characterSetAllowed, options: .CaseInsensitiveSearch) {
    // make sure it's all of them
    return rangeOfCharactersAllowed.count == string.characters.count
  } else  {
    // none of the characters are from the allowed set
    return false
  }
}

保存并運行, 測試一下吧......然后你就會苦逼地發現有BUG.
當嘗試刪除標點符號的時候, 你會發現無法刪除已經存在的標點符號, 這也是為什么我們需要對代碼進行測試, 即使這份代碼看起來非常簡單并且能夠實現預期的功能.這也是為啥我從來不和別人說這就是個簡單的東西, 二十分鐘就能搞定 的原因. 現在我們來修復這個BUG.

當我們嘗試刪除字符的時候, rangeOfCharactersAllowed的值是nil,因為沒有字符被改變(前文有介紹), 因此我們需要添加一個判斷來允許刪除字符.我們一直都在忙著阻止用戶輸入某些特定字符, 同樣的,一旦檢測到string為空的時候,我們也可以允許用戶的輸入嘛.

func textField(textFieldToChange: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
  let characterSetAllowed = NSCharacterSet.punctuationCharacterSet()
  if string.isEmpty
  { // allow deletion
    return true
  }
  else if let rangeOfCharactersAllowed = string.rangeOfCharacterFromSet(characterSetAllowed, options: .CaseInsensitiveSearch)
  {
    // make sure it's all of them
    return rangeOfCharactersAllowed.count == string.characters.count
  }
  else // none of the characters are from the allowed set
  {
    return false
  }
}

處理多個textField

如果你的 view controller是多個textField的代理, 那么就需要對這些textField進行區分.

func textField(textFieldToChange: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
  if textFieldToChange == usernameField {
    // handle username rules
    return shouldChangeUsernameTextField
  } else if textFieldToChange == passwordField {
    // handle password rules
    return shouldChangePasswordTextField
  }
  return true
}

點擊返回鍵的時候收回鍵盤

通常情況下, 點擊返回鍵將會向textField輸入一個換行符,因為textField只能顯示一行,所以實際上點擊返回鍵后什么也不會發生.通過代理方法, 我們可以設置點擊返回后的事件.

func textFieldShouldReturn(textField: UITextField) -> Bool {
  textField.resignFirstResponder()
  return true
}

當app請求textField進行返回的時候我們取消textField的第一響應者標志, 這將會讓鍵盤收回同時移除textField的焦點.

保存輸入的內容

如果需要在一個會進行多次開啟和關閉的app中保存輸入的內容, 我們需要把保存的內容存儲在一個地方, 因為僅僅是保存一些字符, 因此我們可以使用NSUserDefaults來實現.

class ViewController: UIViewController, UITextFieldDelegate {
  @IBOutlet weak var textField: UITextField!
  let textFieldContentsKey = "textFieldContents"

  ...

  func saveText() {
    let defaults = NSUserDefaults.standardUserDefaults()
    defaults.setValue(textField.text, forKey: textFieldContentsKey)
  }
}

在view顯示之前, 我們檢查一下之前知否保存了輸入數據.

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    
    // load text from NSUserDefaults
    let defaults = NSUserDefaults.standardUserDefaults()
    if let textFieldContents = defaults.stringForKey(textFieldContentsKey) {
      textField.text = textFieldContents
    } else {
      // focus on the text field if it's empty
      textField.becomeFirstResponder()
    }
  }
}

textField.becomeFirstResponder()這行代碼讓textField獲取焦點, 并且彈出鍵盤.

編輯完成的時候保存數據

我們需要明確到底什么時候保存輸入數據才是合適的, 最簡單的方法莫過于在編輯結束的時候了,我們可以通過textField的代理來實現

func textFieldDidEndEditing(textField: UITextField) {
  saveText()
}

但是...從用戶體驗上來說, 這絕對不是一個好主意. 如果輸入的時候app突然崩潰腫么辦?一旦發生這個問題, 我們將會喪失所有的輸入.所以, 最好是每點一次鍵盤都保存數據啦.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容