[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突然崩溃肿么办?一旦发生这个问题, 我们将会丧失所有的输入.所以, 最好是每点一次键盘都保存数据啦.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容