本文翻譯自 https://grokswift.com/uitextfield/
在iOS中,Apple為我們提供了三種可以顯示和接收字符輸入的方式:UILabel, UITextField, UITextView, 什么時候該使用哪種方式有時候也會令人非常困惑.
如果你僅僅需要顯示一些文字而不需要輸入文字,那么需要使用UILabel, 有時候你可能會聽到使用UITextView來顯示特殊格式的文字的這種說法, 但是這已經(jīng)過時了, 如今使用AttributedString你也可以做UITextView能做的大部分事情, 一般情況下應(yīng)該首先嘗試使用UILabel, 之后如果真的需要再使用UITextView.
如果你需要接收用戶的輸入,那么你需要使用UItextField或者UITextView, 如果僅僅只有一行文字,你應(yīng)該使用UItextField, 有多行文字的話, 應(yīng)該使用UItextView.
我自己開發(fā)的APP一般有很多UILabel,有一些UITextField,同時只有很少的UITextView,現(xiàn)在我們來看看我們使用UITextField的需求,這些需求我們在開發(fā)過程中是一定會遇到的:
- 限制輸入字符數(shù)量
- 只允許輸入特定字符(或者不允許特定字符)
- 保存輸入的內(nèi)容并且在APP再次打開的時候還原內(nèi)容
- 點擊返回鍵收回鍵盤
我們同時也會接觸到一些UITextField內(nèi)建的顯示方式和行為
建立項目
本案例基于Swift2.0和Xcode7.1
為了一起愉快地玩耍, 我們新建一個SingleViewApplication, 并且拖一個UITextField進去, 給它加上約束.如圖


給UITextField 綁定一個屬性, 同時讓ViewController成為它的代理.
class ViewController: UIViewController, UITextFieldDelegate {
@IBOutlet weak var textField: UITextField!
...
}

現(xiàn)在我們就已經(jīng)準備好來大干一番了.
限制輸入的字符數(shù)量
使用UITextField的時候, 限制輸入的字符數(shù)量是一個十分普遍的要求, 你可以在UITextField的代理方法中實現(xiàn)這個要求.
func textField(textFieldToChange: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
上面這個方法會在textField中輸入的字符發(fā)生改變的時候觸發(fā), 它有三個參數(shù):
-
textFieldToChange: UITextField --哪個
UITextField發(fā)生了改變 - shouldChangeCharactersInRange range: NSRange --發(fā)生改變的跨度(包含開始位置和長度),我們稍后會詳細闡述這個參數(shù)的作用
- replacementString string: String --增加的字符,如果是刪除字符的話, 這個值為空
textField的改變方式可能有很多種情況, 隨之以上三個參數(shù)有多種組合
- 增加字符:
range為空,replacementString的長度是1 - 刪除字符:
range是1(如果是剪切或者選擇了多個字符跨度會更大),replacementString為空 - 在字符中粘貼(一次性增加多個字符):
range為空(因為沒有字符被選擇),replacementString長度大于1 - 清除全部字符(通過剪切操作或者點擊清除按鈕):
range大于1個字符,replacementString為空 - 通過粘貼或者輸入替換了選中的字符:
range大于1,replacementString長度大于1
自動糾正功能和上面的粘貼類似: 可增加多個字符或者替換任意選中的字符
我們無法通過上面的方法來決定一個字符在發(fā)生改變后能否被顯示, 因為一個名為 shouldChangeCharactersInRange的方法會在字符改變之前被觸發(fā),因此我們不能僅僅是在發(fā)生改變之后去檢查字符的變化.
為了限制輸入的字符的數(shù)量,我們不需要知道具體的字符是什么, 僅僅知道他們的長度就足矣.我們需要先計算改變之前的字符的長度, 再加上將要增加的字符的長度, 如果他們的和小于限制數(shù)量則放行, 否則就將超出的部分刪掉.
// 先計算出改變之后的字符串總長度
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
我們使用空合運算來保證無法獲取原始字符串長度的時候?qū)?code>startingLength設(shè)置為 0 (關(guān)于空合運算, 大家可以參考文末的相關(guān)鏈接, 文章的作者只是很簡單的介紹,和主旨不符,不再翻譯)
將計算過程放在具體的代理方法中, 使用一個局部變量characterCountLimit來表示對字符數(shù)量的限制, 之后我們就可以計算出字符的改變是否超出范圍了.
func textField(textFieldToChange: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
// 設(shè)置字符限制為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
}
當(dāng)總長度小于或者等于設(shè)定的限制數(shù)目時會允許輸入,否則不會允許輸入.
現(xiàn)在我們可以運行這個項目并且進行測試看看是否有效果了, 如果是模擬器,還可以使用 CMD + CTRL + Z 來模擬搖晃設(shè)備產(chǎn)生撤銷功能.
禁止輸入某個字符
對輸入進textField的字符進行過濾和進行長度限制其實并沒有什么不同, 我們也是在字符完成輸入之前進行判斷輸入的有效性, 因此我們還是使用和上文相同的代理方法:
func textField(textFieldToChange: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
假設(shè)我們的需求是不允許輸入標點符號, 那么我們可以通過NSCharacterSet來檢查輸入的字符中是否包含標點符號:
let characterSetNotAllowed = NSCharacterSet.punctuationCharacterSet()
如果你需要創(chuàng)建一個自定義的NSCharacterSet, 最簡單的方法是通過String來創(chuàng)建:
let characterSetAllowed = NSCharacterSet(charactersInString: "abcd")
檢查一個string是否包含一個NSCharacterSet中的元素, 我們使用rangeOfCharacterFromSet方法來實現(xiàn):
let rangeOfCharacter = string.rangeOfCharacterFromSet(characterSetNotAllowed, options: .CaseInsensitiveSearch)
上面的rangeOfCharacter包含了characterSetNotAllowed這個NSCharacterSet中的某個元素第一次出現(xiàn)時的位置, 通過它我們可以做我們想做的了, 如果含有標點符號,我們在代理方法中返回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
}
}
現(xiàn)在保存文件并運行, 測試一下我們的代碼(我相信沒什么問題).
**注意: **
本方法只在用戶輸入的時候起作用, 通過代碼直接向textField填寫字符的時候是不起作用的.
只允許某些字符
如果情況變了, 我們希望能夠只允許某些特定的字符被輸入, 其他字符一律不準輸入, 怎么辦? 當(dāng)然還是通過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
}
}
保存并運行, 測試一下吧......然后你就會苦逼地發(fā)現(xiàn)有BUG.
當(dāng)嘗試刪除標點符號的時候, 你會發(fā)現(xiàn)無法刪除已經(jīng)存在的標點符號, 這也是為什么我們需要對代碼進行測試, 即使這份代碼看起來非常簡單并且能夠?qū)崿F(xiàn)預(yù)期的功能.這也是為啥我從來不和別人說這就是個簡單的東西, 二十分鐘就能搞定 的原因. 現(xiàn)在我們來修復(fù)這個BUG.
當(dāng)我們嘗試刪除字符的時候, 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進行區(qū)分.
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只能顯示一行,所以實際上點擊返回鍵后什么也不會發(fā)生.通過代理方法, 我們可以設(shè)置點擊返回后的事件.
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
當(dāng)app請求textField進行返回的時候我們?nèi)∠?code>textField的第一響應(yīng)者標志, 這將會讓鍵盤收回同時移除textField的焦點.
保存輸入的內(nèi)容
如果需要在一個會進行多次開啟和關(guān)閉的app中保存輸入的內(nèi)容, 我們需要把保存的內(nèi)容存儲在一個地方, 因為僅僅是保存一些字符, 因此我們可以使用NSUserDefaults來實現(xiàn).
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顯示之前, 我們檢查一下之前知否保存了輸入數(shù)據(jù).
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獲取焦點, 并且彈出鍵盤.
編輯完成的時候保存數(shù)據(jù)
我們需要明確到底什么時候保存輸入數(shù)據(jù)才是合適的, 最簡單的方法莫過于在編輯結(jié)束的時候了,我們可以通過textField的代理來實現(xiàn)
func textFieldDidEndEditing(textField: UITextField) {
saveText()
}
但是...從用戶體驗上來說, 這絕對不是一個好主意. 如果輸入的時候app突然崩潰腫么辦?一旦發(fā)生這個問題, 我們將會喪失所有的輸入.所以, 最好是每點一次鍵盤都保存數(shù)據(jù)啦.