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

UICollectionView异步加载数据后UI未稳定更新问题求助

问题:异步加载媒体资源后UICollectionView无法稳定更新占位符

我正在开发一款消息应用,从服务器获取消息及关联媒体资源,使用UICollectionView展示消息,初始显示占位符,待实际数据加载完成后替换。

通过多种异步调用获取数据(如KingfisherManager加载图片、FileStorage处理音视频文件、AppleMusicAPI获取音乐曲目),数据获取完成后尝试重载对应section更新单元格,但单元格并非总能更新为已加载的数据,而是持续显示占位符。

核心数据获取与UI更新代码

func createMessage(messageDictionary: [String: Any], completion: @escaping (MKMessage?) -> Void) {
    let message = Message(dictionary: messageDictionary)
    let mkMessage = MKMessage(message: message)

    if message.type == kSPOTIFYLINK {
        print("spotify link")
        let placeholderSpotifyURL = URL(string: "") ?? URL(fileURLWithPath: "")
        let placeholderTrack = SpotifyTrackItem(title: "", artists: [""], album: "", artwork: nil, trackURLPreview: placeholderSpotifyURL, spotifyURL: placeholderSpotifyURL, placeholder: true)
        mkMessage.kind = .custom(placeholderTrack)
        
        // 先检查缓存
        if let cachedTrack = SpotifyCache.object(forKey: message.spotifyLink as NSString) {
            mkMessage.kind = .custom(cachedTrack)
            if let section = mkMessage.section {
                print(section)
                let indexSet = IndexSet(integer: section)
                DispatchQueue.main.async {
                    self.messageCollectionView.messagesCollectionView.reloadSections(indexSet)
                }
            }
        } else {
            spotifyMusicApi.fetchSpotifySongInfo(from: message.spotifyLink) { [weak self] trackItem, error in
                if let trackItem = trackItem {
                    print(trackItem)
                    // 存入缓存
                    self?.SpotifyCache.setObject(trackItem, forKey: message.spotifyLink as NSString)
                    mkMessage.kind = .custom(trackItem)
                    if let section = mkMessage.section {
                        print(section)
                        let indexSet = IndexSet(integer: section)
                        DispatchQueue.main.async {
                            self?.messageCollectionView.messagesCollectionView.reloadSections(indexSet)
                        }
                    }
                } else if let error = error {
                    print(error)
                }
            }
        }
    }
    // 其他类型数据的异步调用逻辑...
}

分页加载旧消息逻辑

override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    if refreshController.isRefreshing {
        getOldMessagesInBackground()
    }
} 

private func getOldMessagesInBackground() {
    var query: Query = FirebaseReference(.Messages).document(FUser.currentId()).collection(chatId)
        .order(by: kDATE, descending: true)
        .limit(to: 10)
    if let lastSnapshot = lastDocumentSnapshot {
        query = query.start(afterDocument: lastSnapshot)
    }

    query.getDocuments { (snapshot, error) in
        guard let snapshot = snapshot else { return }
        
        if snapshot.documents.isEmpty {
            print("no more messages")
            self.messagesCollectionView.refreshControl = nil
            self.areAllMessagesLoaded = true
            return
        }

        let newMessages = ((self.dictionaryArrayFromSnapshot(snapshot.documents)) as NSArray).sortedArray(using: [NSSortDescriptor(key: kDATE, ascending: false)]) as! [Dictionary<String, Any>]
        self.loadedMessageDictionaries = newMessages + self.loadedMessageDictionaries

        self.lastDocumentSnapshot = snapshot.documents.last

        DispatchQueue.main.async {
            self.maxMessageNumber = self.loadedMessageDictionaries.count - self.displayingMessagesCount - 1
            self.minMessageNumber = self.maxMessageNumber - kNUMBEROFMESSAGES
            self.insertOldMessages(newMessages)
            self.messagesCollectionView.reloadDataAndKeepOffset()
            self.refreshController.endRefreshing()
        }
    }
}

private func insertOldMessage(_ messageDictionary: Dictionary<String, Any>) {
    let incoming = IncomingMessage(collectionView_: self)
    incoming.createMessage(messageDictionary: messageDictionary) { [weak self] message in
        guard let self = self, let message = message else { return }
        message.section = 0
        if !self.mkmessages.contains(where: { $0.messageId == message.messageId }) {
            self.mkmessages.insert(message, at: 0)
        } 
    }
}

private func insertOldMessages(_ messages: [Dictionary<String, Any>]) {
    for messageDictionary in messages {
        insertOldMessage(messageDictionary)
        displayingMessagesCount += 1
    }
}

初始消息加载逻辑

private func downloadChats() {
    DispatchQueue.global(qos: .background).async {
        FirebaseReference(.Messages).document(FUser.currentId()).collection(self.chatId).limit(to: 10).order(by: kDATE, descending: true).getDocuments { (snapshot, error) in

            guard let snapshot = snapshot else {
                self.initialLoadCompleted = true
                return
            }

            self.loadedMessageDictionaries = ((self.dictionaryArrayFromSnapshot(snapshot.documents)) as NSArray).sortedArray(using: [NSSortDescriptor(key: kDATE, ascending: true)]) as! [Dictionary<String, Any>]

            self.lastDocumentSnapshot = snapshot.documents.last
            
            if let lastMessageDate = (self.lastDocumentSnapshot?[kDATE] as? Timestamp)?.dateValue() {
               if lastMessageDate > self.latestMessageDate {
                   self.latestMessageDate = lastMessageDate
               }
            }
        
            DispatchQueue.main.async {
                self.insertMessages()
                self.initialLoadCompleted = true
                self.listenForNewChats()
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                    self.messagesCollectionView.reloadData()
                    self.messagesCollectionView.scrollToLastItem(animated: false)
                }
            }
        }
    }
}

private func insertMessages() {
    maxMessageNumber = loadedMessageDictionaries.count - displayingMessagesCount
    minMessageNumber = maxMessageNumber - kNUMBEROFMESSAGES
    
    if minMessageNumber < 0 {
        minMessageNumber = 0
    }
    
    for i in (minMessageNumber ..< maxMessageNumber) {
        let messageDictionary = loadedMessageDictionaries[i]
        insertMessage(messageDictionary)
        displayingMessagesCount += 1
    }
    
    if displayingMessagesCount <= loadedMessageDictionaries.count {
        if messagesCollectionView.refreshControl == nil {
            messagesCollectionView.refreshControl = refreshController
            self.areAllMessagesLoaded = false
        }
    } else {
        messagesCollectionView.refreshControl = nil
    }
}

private func insertMessage(_ messageDictionary: Dictionary<String, Any>) {
    let messageId = messageDictionary[kOBJECTID] as? String
    if let messageId = messageId, !mkmessages.contains(where: { $0.messageId == messageId }) {
        markMessageAsRead(messageDictionary)
        let incoming = IncomingMessage(collectionView_: self)
        incoming.createMessage(messageDictionary: messageDictionary) { [weak self] message in
            guard let self = self, let message = message else { return }
            self.mkmessages.append(message)
            let section = self.mkmessages.count - 1
            message.section = section
            self.messagesCollectionView.performBatchUpdates({
                self.messagesCollectionView.insertSections([self.mkmessages.count - 1])
                if self.mkmessages.count >= 2 {
                    self.messagesCollectionView.reloadSections([self.mkmessages.count - 2])
                }
            }, completion: { [weak self] _ in
                self?.messagesCollectionView.reloadDataAndKeepOffset()
            })
        }
    }
}

推测的问题成因

  • mkMessage的section属性错误:异步任务完成触发UI更新时,section值可能已因消息列表变化而改变,导致UI更新指向错误的单元格。
  • 消息加载导致section索引偏移:重载单元格时,新消息插入或旧消息加载会改变消息列表的section位置,之前存储的section无法指向正确的单元格。
  • 提前调用completion:设置占位符并启动异步操作后立即调用completion,导致消息在异步操作未完成时就被展示,后续数据加载完成后无法正确更新UI。

内容的提问来源于stack exchange,提问作者fun lab

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 07:59:52