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

Swift+UIKit中Firebase Firestore结合SnapshotListener实现分页实时查询

Swift + Firestore 分页查询结合实时更新方案

问题描述

正在使用Swift和UIKit开发一款应用,需实现用户集合的分页查询,数据库采用Firebase Firestore。希望在分页查询的同时实现实时更新,需使用SnapshotListener,但不清楚如何在集合中结合SnapshotListener实现分页。现有代码如下:

internal class UsersViewModel { 
    //MARK: - Propeties
    @Published public var users: [User] = []

    private var lastDocumentSnapshot: QueryDocumentSnapshot?
    
    private let pageSize = 25
    
    private var cancelBag = Set<AnyCancellable>()
    
    private var usersListener: [ListenerRegistration?] = []

    {
        members.removeAll()
        
        guard members.isEmpty, !isFetchingData else {return}
        
        self.isFetchingData = true
        self.lastDocumentSnapshot = nil
        self.membersListener.forEach({ $0?.remove() })
        
        let listener = FIRCollections.space_member_requests(space.id).whereField("credential", isEqualTo: "member").order(by: "last_update_tiemstamp", descending: true).listenDocuments(start: nil, limit: pageSize, callBack: fetchCompletion)
        self.membersListener.append(listener)
        updateMembersData()
    }
    
    public func fetchUsers() {
        users.removeAll()
        usersListener.forEach({ $0?.remove() })
        lastDocumentSnapshot = nil

        guard users.isEmpty else {return}
                
        let listener = FIRCollections.users.listenDocuments(start: nil, limit: pageSize, callBack: fetchCompletion)
        self.usersListener.append(listener)
    }

    private func fetchCompletion(completion: (Result<QuerySnapshot,Error>)) {
        
        if let snapshot = completion.value {
            self.lastDocumentSnapshot = snapshot.documents.last
            
            snapshot.documentChanges.forEach { changes in
                
                if changes.type == .added {
                    if !self.users.contains(where: { $0.id == changes.document.documentID}) {
                        FIRUser_Service.shared.getDocument(changes.document.documentID) { result in
                            if let user = result.value {
                                self.users.append(user)
                            }
                        }
                    }
                } else if changes.type == .removed {
                    if let index = self.members.firstIndex(where: { $0.id == changes.document.documentID}) {
                        self.users.remove(at: index)
                    }
                }
            }
        }
    }
    
    public func updateUsers() {
        guard let lastDocumentSnapshot = lastDocumentSnapshot else {return}
                
        let listener = FIRCollections.users.listenDocuments(start: lastDocumentSnapshot, limit: pageSize, callBack: fetchCompletion)
        self.usersListener.append(listener)
    }
}

extension Query {
    func listenDocuments(start afterDocument: DocumentSnapshot?, limit: Int, callBack: @escaping FIRTypealias_Constants.queryCompletion) -> ListenerRegistration {
        //Gets documents from console
        return self.satrtAfter(afterDocument).limit(to: limit).addSnapshotListener { snapshot, error in
            
            DispatchQueue.main.async {
                if let error = error {
                    callBack(.failure(error))
                } else if let snapshot = snapshot {
                    callBack(.success(snapshot))
                }
            }
        }
    }
}

解决方案

核心思路

  • 只对第一页数据保持实时监听,避免多页监听导致的冗余和数据重复
  • 分页加载后续数据时使用一次性查询(getDocuments),而非持续监听
  • 统一管理监听实例,修复原代码中的变量混淆、拼写错误等问题

修改后的完整代码

1. 扩展Query,区分实时监听与一次性查询

extension Query {
    // 实时监听文档变化(用于第一页数据)
    func listenDocuments(start afterDocument: DocumentSnapshot?, limit: Int, callBack: @escaping (Result<QuerySnapshot, Error>) -> Void) -> ListenerRegistration {
        return self.startAfter(afterDocument).limit(to: limit).addSnapshotListener { snapshot, error in
            DispatchQueue.main.async {
                if let error = error {
                    callBack(.failure(error))
                } else if let snapshot = snapshot {
                    callBack(.success(snapshot))
                }
            }
        }
    }
    
    // 一次性获取文档(用于分页加载后续数据)
    func fetchDocuments(start afterDocument: DocumentSnapshot?, limit: Int, callBack: @escaping (Result<QuerySnapshot, Error>) -> Void) {
        self.startAfter(afterDocument).limit(to: limit).getDocuments { snapshot, error in
            DispatchQueue.main.async {
                if let error = error {
                    callBack(.failure(error))
                } else if let snapshot = snapshot {
                    callBack(.success(snapshot))
                }
            }
        }
    }
}

2. 重构UsersViewModel逻辑

internal class UsersViewModel {
    // MARK: - Properties
    @Published public var users: [User] = []
    @Published private var isFetchingData = false // 防止重复触发分页加载
    
    private var lastDocumentSnapshot: QueryDocumentSnapshot?
    private let pageSize = 25
    private var cancelBag = Set<AnyCancellable>()
    private var usersListener: ListenerRegistration? // 仅保留一个实时监听实例

    // 初始化第一页数据并开启实时监听
    public func fetchUsers() {
        users.removeAll()
        usersListener?.remove() // 移除旧监听
        lastDocumentSnapshot = nil
        isFetchingData = false
        
        // 监听第一页数据的实时变化
        usersListener = FIRCollections.users.listenDocuments(start: nil, limit: pageSize, callBack: handleRealTimeUpdate)
    }

    // 处理实时更新回调
    private func handleRealTimeUpdate(result: Result<QuerySnapshot, Error>) {
        guard let snapshot = try? result.get() else {
            // 可添加错误提示逻辑,例如通知用户监听失败
            return
        }
        
        // 初始化时记录第一页的最后一个文档
        if lastDocumentSnapshot == nil {
            lastDocumentSnapshot = snapshot.documents.last
        }
        
        snapshot.documentChanges.forEach { change in
            let docId = change.document.documentID
            switch change.type {
            case .added:
                // 避免重复添加用户
                guard !users.contains(where: { $0.id == docId }) else { return }
                FIRUser_Service.shared.getDocument(docId) { userResult in
                    if let user = userResult.value {
                        self.users.append(user)
                    }
                }
            case .removed:
                // 移除已删除的用户
                if let index = users.firstIndex(where: { $0.id == docId }) {
                    users.remove(at: index)
                }
            case .modified:
                // 更新用户信息(可选,根据业务需求决定是否保留)
                FIRUser_Service.shared.getDocument(docId) { userResult in
                    if let updatedUser = userResult.value, let index = self.users.firstIndex(where: { $0.id == docId }) {
                        self.users[index] = updatedUser
                    }
                }
            }
        }
    }
    
    // 加载下一页数据(一次性查询)
    public func updateUsers() {
        guard let lastDoc = lastDocumentSnapshot, !isFetchingData else { return }
        isFetchingData = true
        
        FIRCollections.users.fetchDocuments(start: lastDoc, limit: pageSize) { [weak self] result in
            guard let self = self else { return }
            self.isFetchingData = false
            
            guard let snapshot = try? result.get() else {
                // 可添加错误提示逻辑,例如通知用户加载失败
                return
            }
            
            // 更新分页标记
            if let newLastDoc = snapshot.documents.last {
                self.lastDocumentSnapshot = newLastDoc
            }
            
            // 添加新页数据到列表
            for doc in snapshot.documents {
                let docId = doc.documentID
                guard !self.users.contains(where: { $0.id == docId }) else { continue }
                FIRUser_Service.shared.getDocument(docId) { userResult in
                    if let user = userResult.value {
                        self.users.append(user)
                    }
                }
            }
        }
    }
}

关键修改说明

  • 监听实例管理:将usersListener改为单个实例,仅监听第一页数据,避免多页监听导致的资源浪费和数据重复
  • 分页逻辑优化:分页加载使用getDocuments一次性获取数据,而非持续监听,同时通过isFetchingData状态防止重复触发
  • 实时更新覆盖:处理文档的新增、删除、修改三种事件,确保列表数据与Firestore实时同步
  • 修复原代码问题:修正了satrtAfter拼写错误,统一了members与users的变量混淆问题

内容的提问来源于stack exchange,提问作者Santiago Padilla

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 11:35:11