UITextView 输入字数限制
最近需求中有一个常见场景:限制用户输入框的文字长度。乍一看很简单其实坑不少,网上的解决方案都不够严谨,记录一下。
一看这个需求立马想到监听文字变化通知UITextView.textDidChangeNotification
:
NotificationCenter.default.addObserver(self, selector: #selector(onTextDidChange(sender:)), name: UITextView.textDidChangeNotification, object: textView)
var textView: UITextView!
/// 最大字数
var maxTextSize: Int = 20
@objc func onTextDidChange(sender: Notification) {
if(textView.text.count > maxTextSize)
{
textView.text= String(textView.text[..<String.Index(encodedOffset: maxTextSize)])
}
}
然而当使用系统自带键盘输入拼音时,会出现严重问题:你还没有选中要选的文字时,已经被键盘当做字母输入到 textView 当中了,比如用系统键盘输入“你好”,它会把n i h a o
显示在 textView 中, 不但没有输入汉字,还每个字母占两个字符长度。
处理方法是判断系统键盘输入拼音处于高亮状态时不截取字符。
另外注意一点,如果逻辑是先将文字铺上去,再在onTextDidChange
去截取文字的话,被截掉的永远是末尾的文字。而我们希望的是一旦达到字数上限,用户从任意位置开始都无法继续输入文字了。
UITextView 有一个代理方法- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text;
这个方法可以拦截到你即将向 textView 输入的起始位置、个数和具体文字。显然在这个方法进行输入限制更合适。
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
// range: The range of characters to be replaced.(location、count)
// 高亮控制
let selectedRange = textView.markedTextRange
if let selectedRange = selectedRange {
let position = textView.position(from: (selectedRange.start), offset: 0)
if position != nil {
let startOffset = textView.offset(from: textView.beginningOfDocument, to: selectedRange.start)
let endOffset = textView.offset(from: textView.beginningOfDocument, to: selectedRange.end)
let offsetRange = NSMakeRange(startOffset, endOffset - startOffset) // 高亮部分起始位置
if offsetRange.location < maxTextSize {
// 高亮部分先不进行字数统计
return true
} else {
Toast.showFail(message: "字数已达上限")
return false
}
}
}
// 在最末添加
if range.location >= maxTextSize {
Toast.showFail(message: "字数已达上限")
return false
}
// 在其他位置添加
if textView.text.count >= maxTextSize {
Toast.showFail(message: "字数已达上限")
return false
}
return true
}
出现一个新问题,你会发现当字数达到上限时,用户无法删除文字或将选中文字进行 cut 或替代了。因此需要多加一个判断:
...
// 在其他位置添加
if textView.text.count >= maxTextSize && range.length < text.count {
Toast.showFail(message: "字数已达上限")
return false
}
return true
}
这样已经实现了“阻止用户继续输入”的目的。但仅有shouldChangeTextIn:
仍不够,当字数还未达上限,输入联想词和复制粘贴文字导致超出后,就需要进行截取了。如图:
@objc func onTextDidChange(sender: Notification) {
if textView.text.count > maxTextSize {
let selectRange = textView.markedTextRange
if let selectRange = selectRange {
let position = textView.position(from: (selectRange.start), offset: 0)
if (position != nil) {
// 高亮部分不进行截取,否则中文输入会把高亮区域的拼音强制截取为字母,等高亮取消后再计算字符总数并截取
return
}
}
textView.text = String(textView.text[..<String.Index(encodedOffset: maxTextSize)])
// 对于粘贴文字的case,粘贴结束后若超出字数限制,则让光标移动到末尾处
textView.selectedRange = NSRange(location: textView.text.count, length: 0)
}
}
这样便把中文拼音高亮、联想词、复制粘贴等场景都考虑到了。