Search code examples
swiftgoogle-cloud-firestoregeofire

How to get Documents out of an geo query?


I used this function for an geo query. But I don't known how to add the document from the query to an array. So I can display some Map Annotations with infos from an Firestore document. How should I change it?

    func geoQuery() {
        // [START fs_geo_query_hashes]
        // Find cities within 50km of London
        let center = CLLocationCoordinate2D(latitude: 51.5074, longitude: 0.1278)
        let radiusInKilometers: Double = 50

        // Each item in 'bounds' represents a startAt/endAt pair. We have to issue
        // a separate query for each pair. There can be up to 9 pairs of bounds
        // depending on overlap, but in most cases there are 4.
        let queryBounds = GFUtils.queryBounds(forLocation: center,
                                              withRadius: radiusInKilometers)
        let queries = queryBounds.compactMap { (any) -> Query? in
            guard let bound = any as? GFGeoQueryBounds else { return nil }
            return db.collection("cities")
                .order(by: "geohash")
                .start(at: [bound.startValue])
                .end(at: [bound.endValue])
        }

        var matchingDocs = [QueryDocumentSnapshot]()
        // Collect all the query results together into a single list
        func getDocumentsCompletion(snapshot: QuerySnapshot?, error: Error?) -> () {
            guard let documents = snapshot?.documents else {
                print("Unable to fetch snapshot data. \(String(describing: error))")
                return
            }

            for document in documents {
                let lat = document.data()["lat"] as? Double ?? 0
                let lng = document.data()["lng"] as? Double ?? 0
                let coordinates = CLLocation(latitude: lat, longitude: lng)
                let centerPoint = CLLocation(latitude: center.latitude, longitude: center.longitude)

                // We have to filter out a few false positives due to GeoHash accuracy, but
                // most will match
                let distance = GFUtils.distance(from: centerPoint, to: coordinates)
                if distance <= radiusInKilometers {
                    matchingDocs.append(document)
                }
            }
        }

        // After all callbacks have executed, matchingDocs contains the result. Note that this
        // sample does not demonstrate how to wait on all callbacks to complete.
        for query in queries {
            query.getDocuments(completion: getDocumentsCompletion)
        }
        // [END fs_geo_query_hashes]
    }

https://firebase.google.com/docs/firestore/solutions/geoqueries?hl=en#swift_2 This is the Firebase documentary.


Solution

  • I don't know how your documents are structured or how your map is configured to display data (annotations versus regions, for example), but the general fix for your problem is to coordinate the loop of queries in your function and give them a completion handler. And to do that, we can use a Dispatch Group. In the completion handler of this group, you have an array of document snapshots which you need to loop through to get the data (from each document), construct the Pin, and add it to the map. There are a number of other steps involved here that I can't help you with since I don't know how your documents and map are configured but this will help you. That said, you could reduce this code a bit and make it more efficient but let's just go with the Firebase sample code you're using and get it working first.

    struct Pin: Identifiable {
        let id = UUID().uuidString
        var location: MKCoordinateRegion
        var name: String
        var img: String
    }
    
    func geoQuery() {
        // [START fs_geo_query_hashes]
        // Find cities within 50km of London
        let center = CLLocationCoordinate2D(latitude: 51.5074, longitude: 0.1278)
        let radiusInKilometers: Double = 50
        
        // Each item in 'bounds' represents a startAt/endAt pair. We have to issue
        // a separate query for each pair. There can be up to 9 pairs of bounds
        // depending on overlap, but in most cases there are 4.
        let queryBounds = GFUtils.queryBounds(forLocation: center,
                                              withRadius: radiusInKilometers)
        let queries = queryBounds.compactMap { (Any) -> Query? in
            guard let bound = Any as? GFGeoQueryBounds else { return nil }
            return db.collection("cities")
                .order(by: "geohash")
                .start(at: [bound.startValue])
                .end(at: [bound.endValue])
        }
        
        // Create a dispatch group outside of the query loop since each iteration of the loop
        // performs an asynchronous task.
        let dispatch = DispatchGroup()
        
        var matchingDocs = [QueryDocumentSnapshot]()
        // Collect all the query results together into a single list
        func getDocumentsCompletion(snapshot: QuerySnapshot?, error: Error?) -> () {
            guard let documents = snapshot?.documents else {
                print("Unable to fetch snapshot data. \(String(describing: error))")
                dispatch.leave() // leave the dispatch group when we exit this completion
                return
            }
            
            for document in documents {
                let lat = document.data()["lat"] as? Double ?? 0
                let lng = document.data()["lng"] as? Double ?? 0
                let name = document.data()["names"] as? String ?? "no name"
                let coordinates = CLLocation(latitude: lat, longitude: lng)
                let centerPoint = CLLocation(latitude: center.latitude, longitude: center.longitude)
                
                // We have to filter out a few false positives due to GeoHash accuracy, but
                // most will match
                let distance = GFUtils.distance(from: centerPoint, to: coordinates)
                if distance <= radiusInKilometers {
                    matchingDocs.append(document)
                }
            }
            dispatch.leave() // leave the dispatch group when we exit this completion
        }
        
        // After all callbacks have executed, matchingDocs contains the result. Note that this
        // sample does not demonstrate how to wait on all callbacks to complete.
        for query in queries {
            dispatch.enter() // enter the dispatch group on each iteration
            query.getDocuments(completion: getDocumentsCompletion)
        }
        // [END fs_geo_query_hashes]
        
        // This is the completion handler of the dispatch group. When all of the leave()
        // calls equal the number of enter() calls, this notify function is called.
        dispatch.notify(queue: .main) {
            for doc in matchingDocs {
                let lat = doc.data()["lat"] as? Double ?? 0
                let lng = doc.data()["lng"] as? Double ?? 0
                let name = doc.data()["names"] as? String ?? "no name"
                let coordinates = CLLocation(latitude: lat, longitude: lng)
                let region = MKCoordinateRegion(center: <#T##CLLocationCoordinate2D#>, latitudinalMeters: <#T##CLLocationDistance#>, longitudinalMeters: <#T##CLLocationDistance#>)
                let pin = Pin(location: region, name: name, img: "someImg")
                
                // Add pin to array and then to map or just add pin directly to map here.
            }
        }
    }