本文翻譯自 https://grokswift.com/uitextfield/
在iOS中,Apple為我們提供了三種可以顯示和接收字符輸入的方式:UILabel
, UITextField
, UITextView
, 什么時(shí)候該使用哪種方式有時(shí)候也會(huì)令人非常困惑.
如果你僅僅需要顯示一些文字而不需要輸入文字,那么需要使用UILabel
, 有時(shí)候你可能會(huì)聽(tīng)到使用UITextView
來(lái)顯示特殊格式的文字的這種說(shuō)法, 但是這已經(jīng)過(guò)時(shí)了, 如今使用AttributedString
你也可以做UITextView
能做的大部分事情, 一般情況下應(yīng)該首先嘗試使用UILabel
, 之后如果真的需要再使用UITextView
.
如果你需要接收用戶的輸入,那么你需要使用UItextField
或者UITextView
, 如果僅僅只有一行文字,你應(yīng)該使用UItextField
, 有多行文字的話, 應(yīng)該使用UItextView
.
我自己開(kāi)發(fā)的APP一般有很多UILabel
,有一些UITextField
,同時(shí)只有很少的UITextView
,現(xiàn)在我們來(lái)看看我們使用UITextField
的需求,這些需求我們?cè)陂_(kāi)發(fā)過(guò)程中是一定會(huì)遇到的:
- 限制輸入字符數(shù)量
- 只允許輸入特定字符(或者不允許特定字符)
- 保存輸入的內(nèi)容并且在APP再次打開(kāi)的時(shí)候還原內(nèi)容
- 點(diǎn)擊返回鍵收回鍵盤(pán)
我們同時(shí)也會(huì)接觸到一些UITextField
內(nèi)建的顯示方式和行為
建立項(xiàng)目
本案例基于Swift2.0和Xcode7.1
為了一起愉快地玩耍, 我們新建一個(gè)SingleViewApplication, 并且拖一個(gè)UITextField
進(jìn)去, 給它加上約束.如圖
給UITextField
綁定一個(gè)屬性, 同時(shí)讓ViewController成為它的代理.
class ViewController: UIViewController, UITextFieldDelegate {
@IBOutlet weak var textField: UITextField!
...
}
現(xiàn)在我們就已經(jīng)準(zhǔn)備好來(lái)大干一番了.
限制輸入的字符數(shù)量
使用UITextField
的時(shí)候, 限制輸入的字符數(shù)量是一個(gè)十分普遍的要求, 你可以在UITextField
的代理方法中實(shí)現(xiàn)這個(gè)要求.
func textField(textFieldToChange: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
上面這個(gè)方法會(huì)在textField中輸入的字符發(fā)生改變的時(shí)候觸發(fā), 它有三個(gè)參數(shù):
-
textFieldToChange: UITextField --哪個(gè)
UITextField
發(fā)生了改變 - shouldChangeCharactersInRange range: NSRange --發(fā)生改變的跨度(包含開(kāi)始位置和長(zhǎng)度),我們稍后會(huì)詳細(xì)闡述這個(gè)參數(shù)的作用
- replacementString string: String --增加的字符,如果是刪除字符的話, 這個(gè)值為空
textField的改變方式可能有很多種情況, 隨之以上三個(gè)參數(shù)有多種組合
- 增加字符:
range
為空,replacementString
的長(zhǎng)度是1 - 刪除字符:
range
是1(如果是剪切或者選擇了多個(gè)字符跨度會(huì)更大),replacementString
為空 - 在字符中粘貼(一次性增加多個(gè)字符):
range
為空(因?yàn)闆](méi)有字符被選擇),replacementString
長(zhǎng)度大于1 - 清除全部字符(通過(guò)剪切操作或者點(diǎn)擊清除按鈕):
range
大于1個(gè)字符,replacementString
為空 - 通過(guò)粘貼或者輸入替換了選中的字符:
range
大于1,replacementString
長(zhǎng)度大于1
自動(dòng)糾正功能和上面的粘貼類似: 可增加多個(gè)字符或者替換任意選中的字符
我們無(wú)法通過(guò)上面的方法來(lái)決定一個(gè)字符在發(fā)生改變后能否被顯示, 因?yàn)橐粋€(gè)名為 shouldChangeCharactersInRange的方法會(huì)在字符改變之前被觸發(fā),因此我們不能僅僅是在發(fā)生改變之后去檢查字符的變化.
為了限制輸入的字符的數(shù)量,我們不需要知道具體的字符是什么, 僅僅知道他們的長(zhǎng)度就足矣.我們需要先計(jì)算改變之前的字符的長(zhǎng)度, 再加上將要增加的字符的長(zhǎng)度, 如果他們的和小于限制數(shù)量則放行, 否則就將超出的部分刪掉.
// 先計(jì)算出改變之后的字符串總長(zhǎng)度
let startingLength = textFieldToChange.text?.characters.count ?? 0
let lengthToAdd = string.characters.count
let lengthToReplace = range.length
let newLength = startingLength + lengthToAdd - lengthToReplace
在Swift中我們需要通過(guò)字符來(lái)計(jì)算String的長(zhǎng)度
let stringLength = myString.characters.count
我們使用空合運(yùn)算來(lái)保證無(wú)法獲取原始字符串長(zhǎng)度的時(shí)候?qū)?code>startingLength設(shè)置為 0 (關(guān)于空合運(yùn)算, 大家可以參考文末的相關(guān)鏈接, 文章的作者只是很簡(jiǎn)單的介紹,和主旨不符,不再翻譯)
將計(jì)算過(guò)程放在具體的代理方法中, 使用一個(gè)局部變量characterCountLimit
來(lái)表示對(duì)字符數(shù)量的限制, 之后我們就可以計(jì)算出字符的改變是否超出范圍了.
func textField(textFieldToChange: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
// 設(shè)置字符限制為4個(gè)字符
let characterCountLimit = 4
// 先計(jì)算出改變之后的字符串總長(zhǎng)度
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)總長(zhǎng)度小于或者等于設(shè)定的限制數(shù)目時(shí)會(huì)允許輸入,否則不會(huì)允許輸入.
現(xiàn)在我們可以運(yùn)行這個(gè)項(xiàng)目并且進(jìn)行測(cè)試看看是否有效果了, 如果是模擬器,還可以使用 CMD + CTRL + Z 來(lái)模擬搖晃設(shè)備產(chǎn)生撤銷(xiāo)功能.
禁止輸入某個(gè)字符
對(duì)輸入進(jìn)textField
的字符進(jìn)行過(guò)濾和進(jìn)行長(zhǎng)度限制其實(shí)并沒(méi)有什么不同, 我們也是在字符完成輸入之前進(jìn)行判斷輸入的有效性, 因此我們還是使用和上文相同的代理方法:
func textField(textFieldToChange: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
假設(shè)我們的需求是不允許輸入標(biāo)點(diǎn)符號(hào), 那么我們可以通過(guò)NSCharacterSet
來(lái)檢查輸入的字符中是否包含標(biāo)點(diǎn)符號(hào):
let characterSetNotAllowed = NSCharacterSet.punctuationCharacterSet()
如果你需要?jiǎng)?chuàng)建一個(gè)自定義的NSCharacterSet
, 最簡(jiǎn)單的方法是通過(guò)String
來(lái)創(chuàng)建:
let characterSetAllowed = NSCharacterSet(charactersInString: "abcd")
檢查一個(gè)string
是否包含一個(gè)NSCharacterSet
中的元素, 我們使用rangeOfCharacterFromSet
方法來(lái)實(shí)現(xiàn):
let rangeOfCharacter = string.rangeOfCharacterFromSet(characterSetNotAllowed, options: .CaseInsensitiveSearch)
上面的rangeOfCharacter
包含了characterSetNotAllowed
這個(gè)NSCharacterSet
中的某個(gè)元素第一次出現(xiàn)時(shí)的位置, 通過(guò)它我們可以做我們想做的了, 如果含有標(biāo)點(diǎn)符號(hào),我們?cè)诖矸椒ㄖ蟹祷?code>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
}
最后整個(gè)代理方法就像這樣:
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)在保存文件并運(yùn)行, 測(cè)試一下我們的代碼(我相信沒(méi)什么問(wèn)題).
**注意: **
本方法只在用戶輸入的時(shí)候起作用, 通過(guò)代碼直接向textField
填寫(xiě)字符的時(shí)候是不起作用的.
只允許某些字符
如果情況變了, 我們希望能夠只允許某些特定的字符被輸入, 其他字符一律不準(zhǔn)輸入, 怎么辦? 當(dāng)然還是通過(guò)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
}
}
保存并運(yùn)行, 測(cè)試一下吧......然后你就會(huì)苦逼地發(fā)現(xiàn)有BUG.
當(dāng)嘗試刪除標(biāo)點(diǎn)符號(hào)的時(shí)候, 你會(huì)發(fā)現(xiàn)無(wú)法刪除已經(jīng)存在的標(biāo)點(diǎn)符號(hào), 這也是為什么我們需要對(duì)代碼進(jìn)行測(cè)試, 即使這份代碼看起來(lái)非常簡(jiǎn)單并且能夠?qū)崿F(xiàn)預(yù)期的功能.這也是為啥我從來(lái)不和別人說(shuō)這就是個(gè)簡(jiǎn)單的東西, 二十分鐘就能搞定 的原因. 現(xiàn)在我們來(lái)修復(fù)這個(gè)BUG.
當(dāng)我們嘗試刪除字符的時(shí)候, rangeOfCharactersAllowed
的值是nil
,因?yàn)闆](méi)有字符被改變(前文有介紹), 因此我們需要添加一個(gè)判斷來(lái)允許刪除字符.我們一直都在忙著阻止用戶輸入某些特定字符, 同樣的,一旦檢測(cè)到string
為空的時(shí)候,我們也可以允許用戶的輸入嘛.
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
}
}
處理多個(gè)textField
如果你的 view controller
是多個(gè)textField
的代理, 那么就需要對(duì)這些textField
進(jìn)行區(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
}
點(diǎn)擊返回鍵的時(shí)候收回鍵盤(pán)
通常情況下, 點(diǎn)擊返回鍵將會(huì)向textField
輸入一個(gè)換行符,因?yàn)?code>textField只能顯示一行,所以實(shí)際上點(diǎn)擊返回鍵后什么也不會(huì)發(fā)生.通過(guò)代理方法, 我們可以設(shè)置點(diǎn)擊返回后的事件.
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
當(dāng)app請(qǐng)求textField
進(jìn)行返回的時(shí)候我們?nèi)∠?code>textField的第一響應(yīng)者標(biāo)志, 這將會(huì)讓鍵盤(pán)收回同時(shí)移除textField
的焦點(diǎn).
保存輸入的內(nèi)容
如果需要在一個(gè)會(huì)進(jìn)行多次開(kāi)啟和關(guān)閉的app中保存輸入的內(nèi)容, 我們需要把保存的內(nèi)容存儲(chǔ)在一個(gè)地方, 因?yàn)閮H僅是保存一些字符, 因此我們可以使用NSUserDefaults
來(lái)實(shí)現(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
獲取焦點(diǎn), 并且彈出鍵盤(pán).
編輯完成的時(shí)候保存數(shù)據(jù)
我們需要明確到底什么時(shí)候保存輸入數(shù)據(jù)才是合適的, 最簡(jiǎn)單的方法莫過(guò)于在編輯結(jié)束的時(shí)候了,我們可以通過(guò)textField
的代理來(lái)實(shí)現(xiàn)
func textFieldDidEndEditing(textField: UITextField) {
saveText()
}
但是...從用戶體驗(yàn)上來(lái)說(shuō), 這絕對(duì)不是一個(gè)好主意. 如果輸入的時(shí)候app突然崩潰腫么辦?一旦發(fā)生這個(gè)問(wèn)題, 我們將會(huì)喪失所有的輸入.所以, 最好是每點(diǎn)一次鍵盤(pán)都保存數(shù)據(jù)啦.