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

Swift/SwiftUI中Firestore GeoQuery实时监听适配地图区域的实现问询

基于Firestore GeoQuery实现地图区域事件实时监听优化

问题背景

当前实现是在用户滚动地图防抖2秒后,通过GeoFire工具类生成查询边界,批量拉取50公里内的事件。但每次滚动都会重新发起查询,无法实时监听事件的新增/更新/移除,且存在重复请求的问题。需要改为通过GeoQuery实时监听,并支持动态更新监听的中心位置,避免不必要的重复请求。

实现方案

1. 重构ViewModel,添加GeoFire相关管理属性

在ViewModel中维护GeoFire实例、当前GeoQuery、事件监听器集合,统一管理监听生命周期:

class MapViewModel: ObservableObject {
    @Published var coordinateRegion: MKCoordinateRegion
    @Published var eventsInRegion: [DBEvent] = []
    
    private(set) var setOfSubscribers = Set<AnyCancellable>()
    private let geoFire: GeoFire
    private var currentGeoQuery: GeoQuery?
    private var eventDocumentListeners: [String: ListenerRegistration] = [:]
    private var currentListenCenter: CLLocationCoordinate2D?
    private let listenRadiusKm: Double = 50.0 // 监听半径:50公里

    init(initialRegion: MKCoordinateRegion) {
        self.coordinateRegion = initialRegion
        // 初始化GeoFire,指向Events集合
        let eventsCollection = Firestore.firestore().collection("Events")
        self.geoFire = GeoFire(firestoreRef: eventsCollection)
        // 设置地图区域订阅
        setupRegionChangeListener()
    }
}

2. 防抖监听地图区域变化,过滤微小位移

通过Combine监听coordinateRegion变化,防抖后仅当中心位置变化超过100米时才更新GeoQuery,避免频繁请求:

private func setupRegionChangeListener() {
    $coordinateRegion
        .debounce(for: .seconds(2.0), scheduler: RunLoop.main)
        .compactMap { [weak self] region in
            let newCenter = region.center
            guard let currentCenter = self?.currentListenCenter else {
                return newCenter
            }
            // 仅当中心位置变化超过100米时才触发更新
            let distance = GFUtils.distance(from: CLLocation(currentCenter), to: CLLocation(newCenter))
            return distance > 100.0 ? newCenter : nil
        }
        .sink { [weak self] newCenter in
            self?.updateGeoQueryCenter(to: newCenter)
        }
        .store(in: &setOfSubscribers)
}

3. 动态更新GeoQuery,管理事件监听

取消旧的GeoQuery监听,创建新的GeoQuery监听目标区域,同时处理事件的进入/离开/更新:

private func updateGeoQueryCenter(to newCenter: CLLocationCoordinate2D) {
    currentListenCenter = newCenter
    
    // 取消之前的GeoQuery所有监听
    currentGeoQuery?.removeAllObservers()
    // 取消所有事件文档的监听
    eventDocumentListeners.values.forEach { $0.remove() }
    eventDocumentListeners.removeAll()
    // 清空当前事件列表
    eventsInRegion.removeAll()
    
    // 创建新的GeoQuery,监听目标区域
    currentGeoQuery = geoFire.query(at: newCenter, withRadius: listenRadiusKm)
    
    // 监听事件进入范围
    currentGeoQuery?.observe(.keyEntered) { [weak self] eventId, location in
        self?.setupEventDocumentListener(for: eventId)
    }
    
    // 监听事件离开范围
    currentGeoQuery?.observe(.keyExited) { [weak self] eventId, _ in
        self?.eventsInRegion.removeAll { $0.id == eventId }
        self?.eventDocumentListeners[eventId]?.remove()
        self?.eventDocumentListeners.removeValue(forKey: eventId)
    }
    
    // 监听初始加载完成
    currentGeoQuery?.observe(.ready) {
        // 可在此添加加载完成的UI提示逻辑
    }
}

4. 监听单个事件的实时更新

为进入范围的事件添加文档快照监听,确保事件状态(如isActive、startTime)变化时能实时同步到UI:

private func setupEventDocumentListener(for eventId: String) {
    // 避免重复监听同一事件
    guard eventDocumentListeners[eventId] == nil else { return }
    
    let listener = Firestore.firestore().collection("Events").document(eventId)
        .addSnapshotListener { [weak self] snapshot, error in
            guard let self = self,
                  let snapshot = snapshot,
                  snapshot.exists,
                  let event = try? snapshot.data(as: DBEvent.self) else {
                // 事件不存在或解析失败,清理监听和列表
                self.eventsInRegion.removeAll { $0.id == eventId }
                self.eventDocumentListeners[eventId]?.remove()
                self.eventDocumentListeners.removeValue(forKey: eventId)
                return
            }
            
            // 过滤条件:仅保留激活、公开、未开始的事件
            guard event.isActive, event.isPublic, event.startTime > Date() else {
                self.eventsInRegion.removeAll { $0.id == eventId }
                return
            }
            
            // 更新或添加事件到列表
            if let index = self.eventsInRegion.firstIndex(where: { $0.id == eventId }) {
                self.eventsInRegion[index] = event
            } else {
                self.eventsInRegion.append(event)
            }
        }
    
    eventDocumentListeners[eventId] = listener
}

5. 清理资源

在ViewModel销毁时,取消所有监听避免内存泄漏:

deinit {
    currentGeoQuery?.removeAllObservers()
    eventDocumentListeners.values.forEach { $0.remove() }
    setOfSubscribers.forEach { $0.cancel() }
}

关键注意点

  • GeoFire的query(at:withRadius:)方法中,半径单位是公里,需注意和原代码中米单位的转换
  • 添加位置变化阈值(100米)可有效过滤地图微小滚动带来的不必要更新
  • 通过监听单个事件文档的快照,确保事件的状态变更能实时同步到UI
  • 事件离开监听范围时自动移除列表项并取消监听,避免无效资源占用

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 23:57:33