You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

聊天界面滚动加载历史消息异常问题求助

聊天界面滚动加载历史消息重复问题修复

问题描述:聊天界面为避免一次性加载全部消息卡顿,设置初始加载最新20条,滚动时加载更早的20条。初始加载正常,但滚动加载时始终重复显示最新20条,无法正确获取更早的历史消息。

问题根源分析

  1. 错误使用实时监听:加载历史消息时调用addSnapshotListener,每次滚动触发loadMessages都会新增一个实时监听,导致重复接收已加载的最新消息。历史消息加载应该用一次性获取的getDocuments()。
  2. 消息插入位置错误:更早的历史消息时间戳更小,应该插入到messages数组开头,而非追加到末尾,否则排序后会重复处理已有消息。
  3. lastDocumentSnapshot更新时机错误:在documentChanges循环内重复更新,应该在所有文档处理完成后统一更新。
  4. 重复刷新CollectionView:在分组消息时每次追加都调用reloadData,效率低且可能导致显示异常。
  5. 未控制加载状态: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 21:27:35