聊天界面滚动加载历史消息异常问题求助
聊天界面滚动加载历史消息重复问题修复
问题描述:聊天界面为避免一次性加载全部消息卡顿,设置初始加载最新20条,滚动时加载更早的20条。初始加载正常,但滚动加载时始终重复显示最新20条,无法正确获取更早的历史消息。
问题根源分析
- 错误使用实时监听:加载历史消息时调用
addSnapshotListener,每次滚动触发loadMessages都会新增一个实时监听,导致重复接收已加载的最新消息。历史消息加载应该用一次性获取的getDocuments()。 - 消息插入位置错误:更早的历史消息时间戳更小,应该插入到
messages数组开头,而非追加到末尾,否则排序后会重复处理已有消息。 lastDocumentSnapshot更新时机错误:在documentChanges循环内重复更新,应该在所有文档处理完成后统一更新。- 重复刷新CollectionView:在分组消息时每次追加都调用
reloadData,效率低且可能导致显示异常。 - 未控制加载状态:
fetchingMore未在加载完成后重置,可能导致重复触发加载请求。
修正后的代码实现
var lastDocumentSnapshot: DocumentSnapshot! var fetchingMore = false private var messages = [RoomMessage]() private var chatMessages = [[RoomMessage]]() override func viewDidLoad() { super.viewDidLoad() loadMessages() // 单独监听新消息的实时更新 setupRealtimeMessageListener() } // MARK: - Helpers fileprivate func attemptToAssembleGroupedMessages() { chatMessages.removeAll() let groupedMessages = Dictionary(grouping: messages) { element in element.timestampDate.reduceToMonthDayYear() } // 按日期排序分组 let sortedKeys = groupedMessages.keys.sorted() sortedKeys.forEach { key in if let values = groupedMessages[key] { chatMessages.append(values) } } collectionView.reloadData() } // MARK: - API // 加载历史消息(一次性获取) func loadMessages() { guard let room = room, let roomID = room.recentMessage.roomID else { return } showLoader(true) fetchingMore = true var query: Query! if messages.isEmpty { // 初始加载最新20条 query = COLLECTION_ROOMS.document(roomID).collection("messages") .order(by: "timestamp", descending: false) .limit(toLast: 20) print("初始加载20条消息") } else { // 加载更早的20条,用end(before:)定位到上一次加载的第一条消息之前 query = COLLECTION_ROOMS.document(roomID).collection("messages") .order(by: "timestamp", descending: false) .end(beforeDocument: lastDocumentSnapshot) .limit(toLast: 20) print("加载更早的20条消息") } // 用getDocuments()一次性获取,而非实时监听 query.getDocuments { [weak self] querySnapshot, error in guard let self = self else { return } self.showLoader(false) self.fetchingMore = false guard let snapshot = querySnapshot, error == nil else { print("获取消息失败: \(error!)") return } // 更新lastDocumentSnapshot为本次加载的第一条消息(最早的这条) guard let firstSnap = snapshot.documents.first else { print("没有更多历史消息") return } self.lastDocumentSnapshot = firstSnap // 处理获取到的历史消息,插入到数组开头 var newMessages = [RoomMessage]() snapshot.documents.forEach { doc in let message = RoomMessage(dictionary: doc.data()) newMessages.append(message) } // 把新获取的历史消息插入到现有数组的最前面 self.messages.insert(contentsOf: newMessages, at: 0) // 重新分组并刷新UI self.attemptToAssembleGroupedMessages() // 计算插入的indexPaths,用insertItems提升性能(可选) // let insertedIndices = Array(0..<newMessages.count) // let indexPaths = insertedIndices.map { IndexPath(item: $0, section: 0) } // self.collectionView.insertItems(at: indexPaths) } } // 实时监听新消息(只有当有新消息时触发) private func setupRealtimeMessageListener() { guard let room = room, let roomID = room.recentMessage.roomID else { return } let query = COLLECTION_ROOMS.document(roomID).collection("messages") .order(by: "timestamp", descending: false) .limit(toLast: 1) // 只监听最新的一条消息 query.addSnapshotListener { [weak self] snapshot, error in guard let self = self, let snapshot = snapshot, error == nil else { return } snapshot.documentChanges.forEach { change in if change.type == .added { let message = RoomMessage(dictionary: change.document.data()) // 避免重复添加(如果初始加载已经包含这条) if !self.messages.contains(where: { $0.id == message.id }) { self.messages.append(message) self.attemptToAssembleGroupedMessages() // 滚动到最新消息 let lastIndexPath = IndexPath(item: (self.chatMessages.last?.count ?? 0) - 1, section: self.chatMessages.count - 1) self.collectionView.scrollToItem(at: lastIndexPath, at: .bottom, animated: true) } } } } } extension RoomMessageViewController { override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { let contentOffset = scrollView.contentOffset.y // 加上fetchingMore判断,防止重复触发加载 if contentOffset <= -40 && !fetchingMore { loadMessages() } } }
关键修改点说明
- 拆分历史消息加载和新消息实时监听:用
getDocuments()加载历史,addSnapshotListener只监听新消息,避免重复接收旧消息。 - 历史消息插入到数组开头:保证消息顺序正确,且不会和已有消息重复。
- 控制加载状态:
fetchingMore在开始加载时设为true,完成后重置为false,防止同一时间多次触发加载。 - 优化UI刷新:分组完成后统一调用
reloadData,也可以用insertItems提升性能(注释部分)。 - 新消息去重:避免初始加载时已经包含的消息被重复添加。
内容的提问来源于stack exchange,提问作者Unknown
相关产品推荐
相关产品推荐

