How to Query Nearest Users in Firebase with Swift

How to query nearest users in Firebase with Swift?

I'd highly recommend using Geofire for something like this.

To set it up, your data structure will slightly change. You can still store lat/lng on your users, but you will also create a new Firebase table called something like users_locations

Your users_locations table will be populated through Geofire, and will look something like this

users_locations
user_id0:
g: 5pf666y
l:
0: 40.00000
1: -74.00000

In general, this is how you would store a location in Geofire, but you can set it up to save whenever your user object is created / updates location.

let geofireRef = FIRDatabase.database().reference().child("users_locations")
let geoFire = GeoFire(firebaseRef: geofireRef)
geoFire.setLocation(CLLocation(latitude: lat, longitude: lng), forKey: "user_id0")

When you've saved your locations in users_locations, you can then use a GFQuery to query for all the users in a certain range.

let center = CLLocation(latitude: yourLat, longitude: yourLong)
var circleQuery = geoFire.queryAtLocation(center, withRadius: 5)

var queryHandle = circleQuery.observeEventType(.KeyEntered, withBlock: { (key: String!, location: CLLocation!) in
println("Key '\(key)' entered the search area and is at location '\(location)'")
})

How can i get user nearby my location in geofire,Firebase

Keep your GeoFire Locations separate from everything with a key to reference the other additional data e.g. user info or post info. As mixing static and dynamic data would not be the most efficient way of storing the data.

See my data structure and query here:

Retrieving Keys From GeoFire within Radius in Swift

how to get nearby users(from firebase database) from my location using Firestore?

I think code might be working but you might have a logical error in your code.

let visibleRegionn = mapView.projection.visibleRegion()
let bounds = GMSCoordinateBounds(region: visibleRegionn)
// we've got what we want, but here are NE and SW points
let northEast = bounds.northEast
let southWest = bounds.southWest

let geopoint1 = GeoPoint(latitude: northEast.latitude, longitude: northEast.longitude)
let geopoint2 = GeoPoint(latitude: southWest.latitude, longitude: southWest.longitude)

let docRef = Firestore.firestore().collection("Users")
let query =
docRef
.whereField("locationGeopoint", isLessThan: geopoint1)
.whereField("locationGeopoint", isGreaterThan: geopoint2)

query.getDocuments { snapshot, error in
if let error = error {
print("Error getting documents: \(error)")
} else {
for document in snapshot!.documents {
self.arrListMapData.append(document.data() as NSDictionary)
}

}
}

If you see you had - isLessThanOrEqualTo in both places, which actually won't give u the correct results. You need a range so now you want to find all the user around that range so you need to use something like - isLessThan and isGreaterThan.

Hope this helps!

Least data-heavy way to retrieve 5 closest users in Firebase/Swift?

I am very new to this subject, therefore I can only point you in the right direction. Essentially the way you're storing user location is not optimal. The best way to do it is to use GeoFire a newish addition to the Firebase service. How it works is you should have a large location object and you store the geolocation of those users using that users key. Here's an example.

   {
Locations: {
-KH35xPkJmX0UTSG8DuM : {
"g" : "randomID",
"l" : {
"0" : "latitude",
"1" : "longitude"
}
}
}
}

{
Users: {
-KH35xPkJmX0UTSG8DuM : {
"username" : "Joe Sloan"
}
}
}

Your locations object will have 10,000 users keys and geoLocations. the value of "g" and "l" object are set when you use

   geoFire.setLocation(CLLocation(latitude: 37.7853889, longitude: -122.4056973), forKey: "firebase-hq")

Google has optimized the query for users within a similar location.

    let center = CLLocation(latitude: 37.7832889, longitude: -122.4056973)
// Query locations at [37.7832889, -122.4056973] with a radius of 600 meters
var circleQuery = geoFire.queryAtLocation(center, withRadius: 0.6)

The circleQuery variable should contain a Firebase dictionary of the closest users. As I said I haven't had a chance to really delve deeper but this should give you a start.

Can't get near Users by my location with Firebase (GeoFire) and Swift

This answer depends on how you're user data is stored but if you're storing users with their documentId as the users uid then just

let uid = Auth.auth().currentUser!.uid //should safe unwrap that optional

then

geoFire.setLocation(CLLocation(..., forKey: uid)

however, if you want to get it as you're doing in the question

let uid = Auth.auth().currentUser!.uid
let usersCollection = Firestore.firestore().collection("users")
.whereField("uid", isEqualTo: uid)

usersCollection.getDocument(completion: { documentSnapshot, error in
if let err = error {
print(err.localizedDescription)
return
}

guard let docs = documentSnapshot?.documents else { return }

let thisUserUid = docs[0].documentID
print(thisUserUid)
})

But again, that's a bit redundant as it wound indicate storing the uid in both the documentId as well as a child field, which is unnecessary:

users
uid_0 //the documentId
name: "Users name"
uid: "uid_0" //not needed

The problem appers to be actually getting the center point - e.g. if it's not stored, then when read, it will be empty and you'll get that error.

So you have to store it to start with

geoFire.setLocation(CLLocation(latitude: 37.7853889, longitude: -122.4056973), forKey: uid) { (error) in
if (error != nil) {
print("An error occured: \(error)")
} else {
print("Saved location successfully!")
}
}

Once you successfully stored a center point (your location), and then retrieved it, then the actual query should be

let center = CLLocation(userLocation)
geoFire.queryAtLocation(center, withRadius: 20);

Oh, and a very important thing is the setLocation is asynchronous; it takes time for the server to store the data. You should really be working with the data within the closure

geoFire.setLocation(CLLocation(latitude: 37.7853889, longitude: -122.4056973), forKey: uid) { (error) in
// data is now valid so perform your query
}

Sort array by distance near user location from firebase

This is a complex process, requiring multiple steps. I'll try to explain the steps, but you'll have to do quite some work to turn it into a functioning solution.

Geocoding

First up: an address string like the one you have is not a location. There is no way for a computer to compare two of such strings and reliably know how far apart they are.

So the first thing you'll have to do is to turn the addresses into a more reliable indication of a location, i.e. into a latitude and longitude (a.k.a. lat/lon). The process of turning an address into lat/lon is known as geocoding. It is not really an exact science, but there are plenty of services that are quite good at this geocoding bit.

At the end of this geocoding process you will have a lat/lon combination for each address. That puts the problem back into mathematics, which is a much more exact science.

Geoqueries

Next up you'll need to compare the lat/lon of each address and calculate the distance between them. This is a relatively exact science, if you're willing to ignore inaccuracies near the poles and things like that.

Unfortunately the Firebase Realtime Database can natively only order/filter on a single property. Since a location consists of two properties (latitude and longitude) it can't filter on location without some magic.

Luckily somebody came up with a way to translate lat/lon information into a single string, known as a geohash. For example: the Google office in San Francisco is at lat/lon 37.7900515,-122.3923805, which translate to geohash 9q8yyz. The Googleplex in Mountain View is at lat/lon 37.4219999,-122.0862515, which translates to geohash 9q9hvu.

Unlike the addresses you started with, geohashes are very nicely comparable. To quote the (linked) wikipedia explanation:

nearby places [have] similar prefixes. The longer a shared prefix is, the closer the two places are.

In our two examples above, you can see the the two locations are relatively close to each other because they both start with 9q

There is an open-source library for Firebase called GeoFire that:

  1. makes it easy to store locations (for which you must have the lat/lon) in Firebase as geohashes.
  2. provides querying capabilities so that you can get nodes that are within maximum distance of a location you specify.

I recommend that you check out the iOS version of GeoFire.

Query for nearby locations

The Firebase Database can only query by a single property. So the way to filter on latitude and longitude values is to combine them into a single property. That combined property must retain the filtering traits you want for numeric values, such as the ability to filter for a range.

While this at first may seem impossible, it actually has been done in the form of Geohashes. A few of its traits:


  1. It is a hierarchical spatial data structure which subdivides space into buckets of grid shape

So: Geohashes divide space into a grid of buckets, each bucket identified by a string.



  1. Geohashes offer properties like arbitrary precision and the possibility of gradually removing characters from the end of the code to reduce its size (and gradually lose precision).

The longer the string, the larger the area that the bucket covers



  1. As a consequence of the gradual precision degradation, nearby places will often (but not always) present similar prefixes. The longer a shared prefix is, the closer the two places are.

Strings starting with the same characters are close to each other.

Combining these traits and you can see why these Geohashes are so appealing for use with the Firebase Database: they combine the latitude and longitude of a location into a single string, where strings that are lexicographically close to each other point to locations that are physically close to each other. Magic!

Firebase provides a library called Geofire, which uses Geohashes to implement a Geolocation system on top of its Realtime Database. The library is available for JavaScript, Java and Objective-C/Swift.

To learn more about Geofire, check out:

  • this blog post introducing Geofire 2
  • the demo app that used to show local busses moving on a map app showing SF busses.
    The app doesn't work anymore (the data isn't being updated), but the code is still available.
  • this video and documentation on how to implement geoqueries on Cloud Firestore.

Filter only nearest locations from DB

You can use the distance on a CLLocation object, and print out the ones that are within your desired distance.

Something like this:

let userLocation = CLLocation(latitude: userLat, longitude: userLong)
let anotherLocation = CLLocation(latitude: anotherLat, longitude: anotherLong)
let distance = anotherLocation.distance(from: userLocation)

if distance < desiredDistance {
// within range
}

Assemble a list of users with Geofire/Firebase

The data from Geofire and the rest of your Firebase Database is not simply "gotten" from the database. It is asynchronously loaded and then continuously synchronized. This changes the flow of your code. This is easiest to see by adding some logging:

func getNearbyTrucks(){
//Query GeoFire for nearby users
//Set up query parameters
let center = CLLocation(latitude: 37.331469, longitude: -122.029825)
let circleQuery = geoFire.queryAtLocation(center, withRadius: 100)

print("Before Geoquery")

circleQuery.observeEventType(GFEventTypeKeyEntered, withBlock: { (key: String!, location: CLLocation!) in
print("In KeyEntered block ")

let newTruck = Truck()
newTruck.id = key
newTruck.currentLocation = location
self.nearbyTrucks.append(newTruck)

}) //End truckQuery

print("After Geoquery")
}

The output of the logging will be in a different order from what you may expect:

Before Geoquery

After Geoquery

In KeyEntered block

In KeyEntered block

...

While the Geo-keys and users are being retrieved from the server, the code continues and getNearbyTrucks() exits before any keys or users are returned.

One common way to deal with this is to change the way you think of your code from "first load the trucks, then print the firs truck" to "whenever the trucks are loaded, print the first one".

In code this translates to:

func getNearbyTrucks(){
//Query GeoFire for nearby users
//Set up query parameters
let center = CLLocation(latitude: 37.331469, longitude: -122.029825)
let circleQuery = geoFire.queryAtLocation(center, withRadius: 100)

circleQuery.observeEventType(GFEventTypeKeyEntered, withBlock: { (key: String!, location: CLLocation!) in

let newTruck = Truck()
newTruck.id = key
newTruck.currentLocation = location
self.nearbyTrucks.append(newTruck)

print(nearbyTrucks[0].id)
}) //End truckQuery

//Execute code once GeoFire is done with its' query!
circleQuery.observeReadyWithBlock({

for truck in self.nearbyTrucks{

ref.childByAppendingPath("users/\(truck.id)").observeEventType(.Value, withBlock: { snapshot in
print(snapshot.value["name"] as! String)

truck.name = snapshot.value["name"] as! String
truck.description = snapshot.value["selfDescription"] as! String
let base64String = snapshot.value["profileImage"] as! String
let decodedData = NSData(base64EncodedString: base64String as String, options: NSDataBase64DecodingOptions.IgnoreUnknownCharacters)
truck.photo = UIImage(data: decodedData!)!
})
}

}) //End observeReadyWithBlock
}

I've moved the printing of the first truck into the block for the key entered event. Depending on the actual code you're trying to run, you'll move it into different places.

A more reusable approach is the one the Firebase Database and Geofire themselves use: you pass a block into observeEventType withBlock: and that block contains the code to be run when a key is available. If you apply the same pattern to you method, it'd become:

func getNearbyTrucks(withBlock: (key: String) -> ()){
//Query GeoFire for nearby users
//Set up query parameters
let center = CLLocation(latitude: 37.331469, longitude: -122.029825)
let circleQuery = geoFire.queryAtLocation(center, withRadius: 100)

circleQuery.observeEventType(GFEventTypeKeyEntered, withBlock: { (key: String!, location: CLLocation!) in

let newTruck = Truck()
newTruck.id = key
newTruck.currentLocation = location
self.nearbyTrucks.append(newTruck)

withBlock(nearbyTrucks[0].id)
}) //End truckQuery

//Execute code once GeoFire is done with its' query!
circleQuery.observeReadyWithBlock({

for truck in self.nearbyTrucks{

ref.childByAppendingPath("users/\(truck.id)").observeEventType(.Value, withBlock: { snapshot in
print(snapshot.value["name"] as! String)

truck.name = snapshot.value["name"] as! String
truck.description = snapshot.value["selfDescription"] as! String
let base64String = snapshot.value["profileImage"] as! String
let decodedData = NSData(base64EncodedString: base64String as String, options: NSDataBase64DecodingOptions.IgnoreUnknownCharacters)
truck.photo = UIImage(data: decodedData!)!
})
}

}) //End observeReadyWithBlock
}

Here again, you'll want to move the withBlock() callback to a more suitable place depending on your needs.



Related Topics



Leave a reply



Submit