Firebase Realtime Database:如何判断数据集已完成加载?
解决Firebase实时数据库初始加载完成判断与增量更新问题
方案一:通过.observe(.childAdded)感知初始全量加载完成
Firebase的.childAdded事件会先按节点顺序返回所有现有数据,之后才触发新增节点的事件。我们可以结合单次.value查询获取初始数据总数,再通过计数器判断初始加载是否完成:
- 调用
observeSingleEvent(of: .value)获取目标节点的初始快照,记录子节点总数 - 启动
.childAdded监听,每收到一个快照就递增计数器 - 当计数器等于初始总数时,标记初始加载完成,此时触发UITableView的
reloadData();后续的.childAdded事件则用动画更新行
示例代码:
import FirebaseDatabase class AirstripDataManager { private let ref = Database.database().reference().child("Main/PNG/flight/airstrip") private var initialChildCount = 0 private var loadedChildCount = 0 private var isInitialLoadComplete = false private var dataSource: [DataSnapshot] = [] weak var yourTableView: UITableView? // 关联你的表格 func setupListeners() { // 获取初始子节点总数 ref.observeSingleEvent(of: .value) { snapshot in self.initialChildCount = Int(snapshot.childrenCount) if self.initialChildCount == 0 { self.isInitialLoadComplete = true self.updateTableView() } } // 监听childAdded事件 ref.observe(.childAdded) { snapshot in self.dataSource.append(snapshot) self.loadedChildCount += 1 if self.loadedChildCount == self.initialChildCount { self.isInitialLoadComplete = true // 初始全量加载完成,刷新整个表格 self.updateTableView(withAnimation: false) } else if self.isInitialLoadComplete { // 后续新增数据,用动画插入行 let indexPath = IndexPath(row: self.dataSource.count - 1, section: 0) self.updateTableView(withAnimation: true, insertIndexPath: indexPath) } } // 监听childChanged处理修改 ref.observe(.childChanged) { snapshot in guard let index = self.dataSource.firstIndex(where: { $0.key == snapshot.key }) else { return } self.dataSource[index] = snapshot let indexPath = IndexPath(row: index, section: 0) self.updateTableView(withAnimation: true, updateIndexPath: indexPath) } // 监听childRemoved处理删除 ref.observe(.childRemoved) { snapshot in guard let index = self.dataSource.firstIndex(where: { $0.key == snapshot.key }) else { return } self.dataSource.remove(at: index) let indexPath = IndexPath(row: index, section: 0) self.updateTableView(withAnimation: true, deleteIndexPath: indexPath) // 若初始加载未完成,同步调整计数 if !self.isInitialLoadComplete { self.initialChildCount -= 1 if self.loadedChildCount == self.initialChildCount { self.isInitialLoadComplete = true self.updateTableView(withAnimation: false) } } } } private func updateTableView(withAnimation: Bool = false, insertIndexPath: IndexPath? = nil, updateIndexPath: IndexPath? = nil, deleteIndexPath: IndexPath? = nil) { DispatchQueue.main.async { guard let tableView = self.yourTableView else { return } if !withAnimation { tableView.reloadData() } else { tableView.beginUpdates() if let ip = insertIndexPath { tableView.insertRows(at: [ip], with: .automatic) } if let ip = updateIndexPath { tableView.reloadRows(at: [ip], with: .automatic) } if let ip = deleteIndexPath { tableView.deleteRows(at: [ip], with: .automatic) } tableView.endUpdates() } } } }
方案二:避免.childAdded接收初始全量,同时处理遗漏变更
如果不想让.childAdded返回初始数据,需要给节点添加更新时间戳字段(比如updatedAt,用ServerValue.timestamp()设置),然后通过时间过滤实现:
- 调用
observeSingleEvent(of: .value)获取初始快照,同时记录当前服务器时间 - 启动
.childAdded监听时,添加查询条件:只监听updatedAt大于记录时间的节点 - 初始数据从
.value快照中提取,后续的.childAdded只处理新增/修改的节点
示例代码:
import FirebaseDatabase class AirstripDataManager { private let ref = Database.database().reference().child("Main/PNG/flight/airstrip") private var lastLoadedTimestamp: Int64 = 0 private var dataSource: [DataSnapshot] = [] weak var yourTableView: UITableView? // 关联你的表格 func setupListeners() { // 获取初始数据并记录当前服务器时间 ref.observeSingleEvent(of: .value) { snapshot in // 提取初始数据 self.dataSource = snapshot.children.allObjects as! [DataSnapshot] // 获取Firebase服务器时间偏移,计算当前真实服务器时间 Database.database().reference().child(".info/serverTimeOffset").observeSingleEvent(of: .value) { offsetSnapshot in let offset = offsetSnapshot.value as! Int64 self.lastLoadedTimestamp = Int64(Date().timeIntervalSince1970 * 1000) + offset // 初始数据加载完成,刷新表格 self.updateTableView(withAnimation: false) // 启动过滤后的childAdded监听 self.ref.queryOrdered(byChild: "updatedAt") .queryStarting(atValue: self.lastLoadedTimestamp) .observe(.childAdded) { snapshot in self.dataSource.append(snapshot) let indexPath = IndexPath(row: self.dataSource.count - 1, section: 0) self.updateTableView(withAnimation: true, insertIndexPath: indexPath) } } } // 监听childChanged处理修改 ref.observe(.childChanged) { snapshot in guard let index = self.dataSource.firstIndex(where: { $0.key == snapshot.key }) else { return } self.dataSource[index] = snapshot let indexPath = IndexPath(row: index, section: 0) self.updateTableView(withAnimation: true, updateIndexPath: indexPath) } // 监听childRemoved处理删除 ref.observe(.childRemoved) { snapshot in guard let index = self.dataSource.firstIndex(where: { $0.key == snapshot.key }) else { return } self.dataSource.remove(at: index) let indexPath = IndexPath(row: index, section: 0) self.updateTableView(withAnimation: true, deleteIndexPath: indexPath) } } private func updateTableView(withAnimation: Bool = false, insertIndexPath: IndexPath? = nil, updateIndexPath: IndexPath? = nil, deleteIndexPath: IndexPath? = nil) { DispatchQueue.main.async { guard let tableView = self.yourTableView else { return } if !withAnimation { tableView.reloadData() } else { tableView.beginUpdates() if let ip = insertIndexPath { tableView.insertRows(at: [ip], with: .automatic) } if let ip = updateIndexPath { tableView.reloadRows(at: [ip], with: .automatic) } if let ip = deleteIndexPath { tableView.deleteRows(at: [ip], with: .automatic) } tableView.endUpdates() } } } }
注意事项
- 方案一的计数器方式需注意:若初始加载期间有节点被删除/新增,要同步调整
initialChildCount,避免计数偏差 - 方案二需要确保所有节点都有
updatedAt字段,新增/修改节点时要同步更新(比如写数据时加上["updatedAt": ServerValue.timestamp()]) - 所有UI更新必须在主线程执行,避免卡顿
内容的提问来源于stack exchange,提问作者woodipw
相关产品推荐
相关产品推荐

