Created
November 4, 2023 11:39
-
-
Save calvingit/b4a86a0f5dce8a69e65160bf5d0b60dc to your computer and use it in GitHub Desktop.
限制 UITextField 和 UITextView 的输入
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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