Skip to content

Instantly share code, notes, and snippets.

@calvingit
Created November 4, 2023 11:39
Show Gist options
  • Select an option

  • Save calvingit/b4a86a0f5dce8a69e65160bf5d0b60dc to your computer and use it in GitHub Desktop.

Select an option

Save calvingit/b4a86a0f5dce8a69e65160bf5d0b60dc to your computer and use it in GitHub Desktop.
限制 UITextField 和 UITextView 的输入
import UIKit
import IQKeyboardManager
/// 语音播报
class SecurityActionVoiceContentViewController: UIViewController {
lazy var textView = IQTextView().do {
$0.font = .systemFont(ofSize: 15)
$0.textColor = UIColor(hexString: "#333333")
$0.backgroundColor = .white
$0.layer.cornerRadius = 10
}
lazy var tipsLabel = UILabel().do {
$0.font = .systemFont(ofSize: 14)
$0.textColor = UIColor(hexString: "#333333")
$0.text = L10n("security.config.action.voice.tips")
}
lazy var countLabel = UILabel().do {
$0.font = .systemFont(ofSize: 14)
$0.textColor = UIColor(hexString: "#333333")
}
// 最大字符数
let maxCharsCount = 200
var textLimitDelegate: TextInputLimitDelegate?
var text: String!
var completion: ((String) -> Void)?
convenience init(text: String, completion: ((String) -> Void)?) {
self.init(nibName: nil, bundle: nil)
self.text = text
self.completion = completion
}
override func viewDidLoad() {
super.viewDidLoad()
title = L10n("Localizable_LinkageOutPut_Voice")
view.backgroundColor = UIColor(hexString: "#F5F5F8")
navigationItem.rightBarButtonItem = UIBarButtonItem(title: L10n("Save"), style: .plain, target: self, action: #selector(saveAction))
setupViews()
textView.text = text
textView.placeholder = L10n("security.config.action.voice.placeholder")
textView.placeholderTextColor = .lightGray
textLimitDelegate = TextInputLimitDelegate(allowedRegular: TextInputLimitDelegate.nameRex,
maxLength: 200, textChanged: { [weak self] text in
self?.updateTextCount(text)
})
textLimitDelegate?.setInputView(textView)
updateTextCount(text)
}
@objc func saveAction() {
textView.resignFirstResponder()
completion?(textView.text)
}
func updateTextCount(_ text: String?) {
countLabel.text = "\(text?.lengthInBytes() ?? 0)/\(200)"
}
func setupViews() {
view.addSubview(textView)
view.addSubview(tipsLabel)
view.addSubview(countLabel)
textView.layout { [
$0.topAnchor.equalTo(view.safeAreaLayoutGuide.topAnchor).offset(10),
$0.leadingAnchor.equalTo(view.leadingAnchor).offset(20),
$0.trailingAnchor.equalTo(view.trailingAnchor).offset(-20),
$0.heightAnchor.equal(160),
] }
tipsLabel.layout { [
$0.topAnchor.equalTo(textView.bottomAnchor).offset(10),
$0.leadingAnchor.equalTo(textView.leadingAnchor),
] }
countLabel.layout { [
$0.topAnchor.equalTo(textView.bottomAnchor).offset(10),
$0.trailingAnchor.equalTo(textView.trailingAnchor),
] }
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
textView.resignFirstResponder()
}
}
extension String {
/// 计算字节数,中文算非ASCII字符,占2个字节
func lengthInBytes() -> Int {
var length = 0
for char in self {
if char.isASCII {
length += 1
} else {
length += 2
}
}
return length
}
func removeEmoji() -> String {
self.filter {
$0.unicodeScalars.allSatisfy({ scalar in
!scalar.properties.isEmojiPresentation
})
}
}
var containEmoji: Bool {
unicodeScalars.contains { scalar in
scalar.properties.isEmojiPresentation
}
}
}
/// 文本输入限制
@objc
public class TextInputLimitDelegate: NSObject, UITextFieldDelegate, UITextViewDelegate {
// 中英文 + 数字 + 空格
@objc static let nameRex = "[a-zA-Z0-9\\u4E00-\\u9FA5\\s]"
// 纯数字
@objc static let phoneNumberRex = "^[0-9]+$"
// 文本变化
@objc var textChanged: ((String?) -> Void)?
// 最大长度,0为不限制(计算字节数,中文算非ASCII字符,占2个字节)
@objc var maxLength: UInt = 0
// 允许输入的正则表达式
@objc var allowedRegular: String = ""
// 是否允许换行,针对 UITextView
@objc var allowLineBreak = false
@objc convenience init(allowedRegular: String, maxLength: UInt, textChanged: ((String?) -> Void)?) {
self.init()
self.allowedRegular = allowedRegular
self.maxLength = maxLength
self.textChanged = textChanged
}
@objc func setInputView(_ inputView: UIView) {
switch inputView {
case let textField as UITextField:
textField.delegate = self
textField.addTarget(self, action: #selector(textFieldChangedText(_:)), for: .editingChanged)
case let textView as UITextView:
textView.delegate = self
default:
break
}
}
private override init() {
super.init()
}
func inputView(_ inpuText: String?, textInputMode: UITextInputMode?, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
// 删除按钮点击
if text.isEmpty {
return true
}
// 禁止 emoji 输入
guard let language = textInputMode?.primaryLanguage,
language != "emoji" else {
return false
}
// 如果是中文输入法,九宫格输入法的 A-Z 字母点击时,预览会显示➋这样的占位符,此时用户还没有真正的选择
// 如果输入 "ma",updatedText 会是 "m➋"
let nineChars = "➋➌➍➎➏➐➑➒"
if text.contains(where: { nineChars.contains($0) }) {
return true
}
// 包含 emoji
if text.containEmoji {
return false
}
// 正则为空就不判断了
if allowedRegular.isEmpty {
return true
}
return validateInput(regexPattern: allowedRegular, inputString: text)
}
// 验证输入的字符串是否符合正则表达式
func validateInput(regexPattern: String, inputString: String) -> Bool {
do {
let regex = try NSRegularExpression(pattern: regexPattern, options: .caseInsensitive)
let range = NSRange(location: 0, length: inputString.count)
let matches = regex.matches(in: inputString, options: [], range: range)
return matches.count > 0
} catch {
print("Invalid regex pattern: \(error)")
return false
}
}
// 限制输入长度
func changedText(_ text: String?) -> String {
guard var text else { return "" }
Logger.debug("输入: \(text)")
if !allowLineBreak {
// 禁止换行
text = text.replacingOccurrences(of: "\n", with: "")
}
while (text.lengthInBytes() > maxLength) {
text.removeLast()
}
return text
}
// 编辑结束的时候,将首尾的空白去掉
func trimText(_ text: String?) -> String {
guard let text else { return "" }
return text.trimmingCharacters(in: .whitespacesAndNewlines)
}
// MARK: UITextFieldDelegate
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if (!textField.isFirstResponder) {
return true
}
return inputView(textField.text, textInputMode: textField.textInputMode, shouldChangeTextIn: range, replacementText: string)
}
@objc func textFieldChangedText(_ textField: UITextField) {
textField.text = changedText(textField.text)
textChanged?(textField.text)
}
public func textFieldDidEndEditing(_ textField: UITextField) {
textField.text = trimText(textField.text)
textChanged?(textField.text)
}
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
// MARK: UITextViewDelegate
public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
inputView(textView.text, textInputMode: textView.textInputMode, shouldChangeTextIn: range, replacementText: text)
}
public func textViewDidChange(_ textView: UITextView) {
textView.text = changedText(textView.text)
textChanged?(textView.text)
}
public func textViewDidEndEditing(_ textView: UITextView) {
textView.text = trimText(textView.text)
textChanged?(textView.text)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment