Swift实现类Facebook@提及用户功能 选中用户后监听不停止问题
iOS实现@用户提及功能异常修复
问题描述
尝试实现类似Facebook的@用户提及效果:当输入字符串包含@符号时,在TableView中拉取展示用户列表。但目前存在异常:只要文本中出现@符号就会持续触发用户列表监听逻辑,即便已经选中目标用户、将用户名插入输入文本后,监听仍不会停止。
初始问题代码如下:
override func viewDidLoad() { super.viewDidLoad() NotificationCenter.default.addObserver(self, selector: #selector(checkUsername), name: UITextView.textDidChangeNotification, object: nil) } @objc func checkUsername() { if (textView.text.range(of: "@" + "") != nil) { let range: Range<String.Index> = self.containerView.commentTextView.text.range(of: "@" + "")! self.textView.text.replaceSubrange(range, with: " ") } } let usernames = ["Ariana", "Brad", "Chelsea"]
问题根因
- 触发条件过于宽泛:只要全文本存在任意
@字符就触发逻辑,完全不区分@是待输入的触发点,还是已经插入完成的用户名的组成部分(比如已经插入的@Ariana里的@也会被识别为触发条件),自然会持续触发监听。 - 匹配逻辑错误:
range(of: "@" + "")本质是匹配全文第一个出现的@,没有定位光标附近的有效触发点,甚至代码里直接把匹配到的@替换为空格,会直接破坏已经插入完成的提及内容。 - 缺少状态闭环:选中用户插入用户名后,没有任何逻辑退出搜索状态,无法区分「正在输入提及内容」和「提及内容已经插入完成」两种场景。
修复方案
核心逻辑调整:仅当光标紧邻符合规则的@(@位于文本开头或空白字符之后,且@到光标位置之间没有其他空白字符)时,才激活用户列表搜索;选中用户插入完成后,主动重置搜索状态隐藏列表,避免被已插入内容中的@重复触发。
修复后的完整实现代码:
import UIKit class CommentViewController: UIViewController, UITextViewDelegate, UITableViewDataSource, UITableViewDelegate { @IBOutlet weak var commentTextView: UITextView! @IBOutlet weak var userListTableView: UITableView! // 预置用户列表 let usernames = ["Ariana", "Brad", "Chelsea"] // 标记是否处于@提及搜索激活状态 private var isMentionActive = false // 记录当前触发搜索的@所在位置 private var mentionStartIndex: String.Index? // 搜索匹配到的用户 private var matchedUsers: [String] = [] { didSet { userListTableView.reloadData() userListTableView.isHidden = matchedUsers.isEmpty } } override func viewDidLoad() { super.viewDidLoad() commentTextView.delegate = self userListTableView.isHidden = true userListTableView.dataSource = self userListTableView.delegate = self // 移除原全局通知监听,改用代理回调更可控 } // 检测当前文本是否触发@提及 private func checkMentionStatus() { guard let cursorRange = commentTextView.selectedTextRange, let cursorPos = commentTextView.position(from: cursorRange.start, offset: 0) else { resetMentionStatus() return } // 获取光标前的文本内容 let cursorOffset = commentTextView.offset(from: commentTextView.beginningOfDocument, to: cursorPos) let textBeforeCursor = String(commentTextView.text.prefix(cursorOffset)) var searchPos = textBeforeCursor.endIndex var validMentionStart: String.Index? // 从光标位置向前查找最近的有效@ while searchPos > textBeforeCursor.startIndex { searchPos = textBeforeCursor.index(before: searchPos) let currentChar = textBeforeCursor[searchPos] if currentChar == "@" { // 校验@前面是否为文本开头或空白字符,避免匹配邮箱、已完成提及中的@ if searchPos == textBeforeCursor.startIndex { validMentionStart = searchPos break } let prevChar = textBeforeCursor[textBeforeCursor.index(before: searchPos)] if prevChar.isWhitespace || prevChar.isNewline { validMentionStart = searchPos break } } // 遇到空白字符则停止查找,说明当前光标前没有待输入的@提及 if currentChar.isWhitespace || currentChar.isNewline { break } } guard let mentionStart = validMentionStart else { resetMentionStatus() return } // 提取@后的搜索关键词 let keywordStart = textBeforeCursor.index(after: mentionStart) let keyword = String(textBeforeCursor[keywordStart...]) // 更新搜索状态 isMentionActive = true mentionStartIndex = mentionStart // 匹配用户 if keyword.isEmpty { matchedUsers = usernames } else { matchedUsers = usernames.filter { $0.lowercased().hasPrefix(keyword.lowercased()) } } } // 重置提及搜索状态 private func resetMentionStatus() { isMentionActive = false mentionStartIndex = nil matchedUsers = [] } // 选中用户后插入用户名 private func insertMentionUser(_ username: String) { guard let startIndex = mentionStartIndex else { return } // 计算光标在全文本中的位置,替换@到光标位置的内容 let cursorLocation = commentTextView.selectedRange.location let replaceEnd = commentTextView.text.index(commentTextView.text.startIndex, offsetBy: cursorLocation) let replaceRange = startIndex..<replaceEnd // 插入格式为@用户名 + 空格 commentTextView.text.replaceSubrange(replaceRange, with: "@\(username) ") // 移动光标到插入内容末尾 let newCursorLocation = commentTextView.text.distance(from: commentTextView.text.startIndex, to: replaceRange.lowerBound) + username.count + 2 commentTextView.selectedRange = NSRange(location: newCursorLocation, length: 0) // 重置状态隐藏用户列表 resetMentionStatus() } // MARK: - UITextViewDelegate func textViewDidChange(_ textView: UITextView) { checkMentionStatus() } func textViewDidChangeSelection(_ textView: UITextView) { // 光标移动时校验状态,避免光标移走后列表仍显示 checkMentionStatus() } // MARK: - UITableViewDataSource/Delegate func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { matchedUsers.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell", for: indexPath) cell.textLabel?.text = matchedUsers[indexPath.row] return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) insertMentionUser(matchedUsers[indexPath.row]) } }
关键优化点
- 移除全局通知监听,改用
UITextViewDelegate回调处理文本变化,避免全局通知带来的非预期触发 - 新增有效
@校验规则,仅识别光标前、位于文本开头/空白字符后的@作为触发点,不会误判已插入完成的提及内容、邮箱地址中的@ - 加入完整状态管理,选中用户插入内容后主动重置搜索状态,插入完成后自动隐藏用户列表,不会重复触发监听
- 支持根据
@后输入的关键词实时过滤用户列表,交互逻辑和主流社交产品一致
内容的提问来源于stack exchange,提问作者JuFa512
相关产品推荐
相关产品推荐

