[iOS] UITextField基礎(chǔ)和一些高級(jí)功能

本文翻譯自 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ù):

  1. textFieldToChange: UITextField --哪個(gè)UITextField發(fā)生了改變
  2. shouldChangeCharactersInRange range: NSRange --發(fā)生改變的跨度(包含開(kāi)始位置和長(zhǎng)度),我們稍后會(huì)詳細(xì)闡述這個(gè)參數(shù)的作用
  3. 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ù)啦.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末官套,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子降狠,更是在濱河造成了極大的恐慌锭硼,老刑警劉巖翩剪,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腕柜,死亡現(xiàn)場(chǎng)離奇詭異膀篮,居然都是意外死亡毯焕,警方通過(guò)查閱死者的電腦和手機(jī)衍腥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)纳猫,“玉大人婆咸,你說(shuō)我怎么就攤上這事∥咴” “怎么了尚骄?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)侵续。 經(jīng)常有香客問(wèn)我倔丈,道長(zhǎng),這世上最難降的妖魔是什么状蜗? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任需五,我火速辦了婚禮,結(jié)果婚禮上轧坎,老公的妹妹穿的比我還像新娘宏邮。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布蜜氨。 她就那樣靜靜地躺著械筛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪飒炎。 梳的紋絲不亂的頭發(fā)上埋哟,一...
    開(kāi)封第一講書(shū)人閱讀 52,441評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音厌丑,去河邊找鬼定欧。 笑死,一個(gè)胖子當(dāng)著我的面吹牛怒竿,可吹牛的內(nèi)容都是我干的砍鸠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼耕驰,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼爷辱!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起朦肘,我...
    開(kāi)封第一講書(shū)人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤饭弓,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后媒抠,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體弟断,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年趴生,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了阀趴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡苍匆,死狀恐怖刘急,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情浸踩,我是刑警寧澤叔汁,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站检碗,受9級(jí)特大地震影響据块,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜后裸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一瑰钮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧微驶,春花似錦浪谴、人聲如沸开睡。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)篇恒。三九已至,卻和暖如春凶杖,著一層夾襖步出監(jiān)牢的瞬間胁艰,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工智蝠, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留腾么,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓杈湾,卻偏偏與公主長(zhǎng)得像解虱,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子漆撞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容