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
相关产品推荐
相关产品推荐

