本文翻譯自 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中輸入的字符發生改變的時候觸發, 它有三個參數:
-
textFieldToChange: UITextField --哪個
UITextField
發生了改變 - shouldChangeCharactersInRange range: NSRange --發生改變的跨度(包含開始位置和長度),我們稍后會詳細闡述這個參數的作用
- 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突然崩潰腫么辦?一旦發生這個問題, 我們將會喪失所有的輸入.所以, 最好是每點一次鍵盤都保存數據啦.