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