Search code examples
iosswiftfirebasegeolocation

Swift Firebase -How to get individual user's location from GeoFire


1- All the users in my app send their location to GeoFire every 2.5 minutes using a Timer.

2- Other users also query GeoFire to look for whichever users are in a 1 mile radius from them (eg. 10 users). I get those 10 users and then and add them to an array.

3- I then loop through the array with those 10 users using their userId (the geoRef key). I go to their Database ref and search to see if they fit some criteria. If they do I add them to a different array (eg 5 users are now in this subSet array)

4- Since every 2.5 minutes every user's location is sent to GeoFire, that means those 5 users in that subset might have a different location from when they were first added to the subset array.

I can use a timer to query the location on those 5 users. The question is how do I query GeoFire to get the location of each individual user from only those 5 users? I don't want query everyone within that 1 mile region again otherwise it's going to get me those same 10 users

// I have a Timer firing this off
func queryLocationOfSubsetOfUsersInRadius() {

    let geofireRef = Database.database().reference().child("geoLocations")
    let geoFire = GeoFire(firebaseRef: geoFireRef)

    let dispatchGroup = DispatchGroup()

    for user in subsetOfUsersInRadius {

        dispatchGroup.enter()

        let userId = user.userId

        // I don't know if this is the right method to use. I only added it here because I saw it has observeValue on it
        geoFire.observeValue(forKeyPath: userId, of: Any?, change: NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)

            // *** HOW TO GET THE USERS NEW LOCATION and use dispatchGroup.leave() as each one is obtained??? ***

            dispatchGroup.leave()
    }

    dispatchGroup.notify(queue: .global(qos: .background)) {

       // now animate the annotation from the user's inital old location (if they moved) on the mapView to their new location on the mapView. It's supposed to look like Uber's cars moving. Happens on main thread
    }
}

Supporting code below

var queryHandle: UInt?
var regionQuery: GFRegionQuery?

var usersInRadius = [User]() // has 10 users
var subsetOfUsersInRadius = [User]() // of the 10 only 5 fit some criteria

let geofireRef = Database.database().reference().child("geoLocations")
let geoFire = GeoFire(firebaseRef: geofireRef)

// region: MKCoordinateRegion was previously set at 1 mile 1609.344 meters

regionQuery = geoFire.query(with: region)
queryHandle = regionQuery?.observe(.keyEntered, with: { [weak self](key: String!, location: CLLocation!) in

    let user = User()
    user.userId = key
    user.location = location

    self?.usersInRadius.append(user)
})

regionQuery?.observeReady({ [weak self] in

    self?.sortUsersInRadius(arr: self!.usersInRadius)
})

func sortUsersInRadius(arr: [User]) {

    if let queryHandle = queryHandle {
        regionQuery?.removeObserver(withFirebaseHandle: queryHandle)
    }

    let dispatchGroup = DispatchGroup()

    for user in arr {

        let userId = user.userId

        someDatabaseRef.child(userId).observeSingleEvent(of: .value, with: { (snapshot) in

             // if snapshot contains some critera add that user to the subSet array

             self?.subsetOfUsersInRadius.append(user) // only 5 users fit this criteria

             dispatchGroup.leave()
        })
    }

    dispatchGroup.notify(queue: .global(qos: .background)) {

       // add an annotation to mapView to show the initial location of each user from subsetOfUsersInRadius. Happens on main thread
    }
}

Solution

  • Inside the Database the GeoFire ref of each userId location has a "g" child and a "l" child:

    @geoLocations
          |
          @--abc123xyz456 // userId
               |
               @--g: "dr72xyz25abc" // geoFire id for this user's location in geoFire
               |
               @--l
                  |--0: 40.870431300779900 // latitude
                  |--1: -73.090007211987188 // longitude
    

    Here's a picture of the actual database layout

    enter image description here

    I have no idea what "g" stands for but I assume "l" stands for location because it's of type CLLocation as is stated in the arguments of .observe(.keyEntered, with: { (key: String!, location: CLLocation!).

    Inside the database the 0 key and 1 are held as snapshot.value of array of either CLLocationDegrees or Double.

    To get the latitude and longitude I used let arr = snapshot.value as? [CLLocationDegrees] but let arr = snapshot.value as? [Double] also worked.

    Create a ref that has a child name of whatever the name of your geoLocations ref is > then add a child of the the userId > then add a child of "l" for the locations child.

    Run observeSingleEvent(of: .value and in the callback cast snapshot.value as an an array of [CLLocationDegrees]

    // *** if using CLLocationDegrees be to import CoreLocation ***
    import CoreLocation
    
    let geoLocationsRef = Database.database().reference()
                            .child("geoLocations") // name of my geoRef in Firebase
                            .child("abc123xyz456") // the userId I'm observing
                            .child("l") // the "l" is the child to observe
    
    geoLocationsRef.observeSingleEvent(of: .value, with: { (snapshot) in
    
        if !snapshot.exists() { return }
    
        guard let arr = snapshot.value as? [CLLocationDegrees] else { return }
    
        if arr.count > 1 {
    
            let latitude = arr[0]
            print(latitude)
    
            let longitude = arr[1]
            print(longitude)
    
            // do whatever with the latitude and longitude
        }
    })
    

    Here is the answer to my question with using dispatchGroup():

    func queryLocationOfSubsetOfUsersInRadius() {
    
        let dispatchGroup = DispatchGroup()
    
        for user in subsetOfUsersInRadius {
    
            dispatchGroup.enter()
    
            let userId = user.userId
    
            let geoLocationsRef = Database.database().reference()
                .child("geoLocations")
                .child(userId)
                .child("l")
    
            geoLocationsRef.observeSingleEvent(of: .value, with: { (snapshot) in
    
                // this user may have deleted their location
                if !snapshot.exists() {
                    dispatchGroup.leave()
                    return
                }
    
                guard let arr = snapshot.value as? [CLLocationDegrees] else {
                    dispatchGroup.leave()
                    return
                }
    
                if arr.count > 1 {
    
                    let latitude = arr[0]
                    print(latitude)
    
                    let longitude = arr[1]
                    print(longitude)
    
                    // do whatever with the latitude and longitude
                }
    
                dispatchGroup.leave()
            })
        }
    
        dispatchGroup.notify(queue: .global(qos: .background)) {
    
            // now animate the annotation from the user's inital old location (if they moved) on the mapView to their new location on the mapView. It's supposed to look like Uber's cars moving. Happens on main thread
        }
    }