I've tried for so many weeks to get it to work. I have read the documentation many times and there seem to be no examples of this online.
Goal:
All I want to be able to do is print to the console all the posts that are within a 10km radius of the current user's location.
Issues: I don't really understand what parameters need to placed into the geoFire.setLocation the user's locations or post location. The documentation only shows a manually entered coordinates. I want to pull mine from firebase and query it for when the user gets within 10km they are printed to the console. Currently, with the code I have nothing is being printed at all.
fileprivate func setupGeoFireLocation() {
let ref = Database.database().reference(withPath: "posts")
ref.observe(.childAdded, with: { (snapshot) in
guard let dictionary = snapshot.value as? [String: Any] else { return }
guard let latitude = dictionary["latitude"] as? String else { return }
guard let longitude = dictionary["longitude"] as? String else { return }
let postLat = (latitude as! NSString).doubleValue
let postLon = (longitude as! NSString).doubleValue
self.geoFire.setLocation(CLLocation(latitude: postLat, longitude: postLat), forKey: "posts")
//Not quite sure what's meant to be the "forKey:" parameter.
})
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let userLocation: CLLocation = locations[0] as CLLocation
ref = Database.database().reference()
geoFire = GeoFire(firebaseRef: ref)
let center = CLLocation(latitude: userLocation.coordinate.latitude, longitude: userLocation.coordinate.longitude)
let circleQuery = geoFire.query(at: center, withRadius: 10.0)
_ = circleQuery.observe(.keyEntered, with: { (key, location) in
print(key)
})
circleQuery.observeReady{
print("All initial data has been loaded and events have been fired for circle query!")
}
}
From your comments it seems like you never specifically set the location information through GeoFire's setLocation method. For GeoQueries to work you need to indeed set the location in GeoFire as well. It won't simply work from your existing lat and lon values.
GeoFire queries a separate location, where it associates keys with geohashes of the location info. Please follow the GeoFire documentation on setting a location for a key, retrieving a location, and querying.
I also recommend reading these questions that explain more about GeoFire and how it works:
Filtering results with Geofire + Firebase (shows the data structure that you're missing)
GeoFire query on User location (shows that there are two top-level nodes: one with the geo-info only, and one with the other information. You currently only have the latter.)
Using GeoFire queries in Swift does not give useful output
Android GeoFire onKeyEntered not trigged
Related
Please how can make it in real time ?
like adding addSnapshotListener.
This code is only work when it called, I wanna if you change in FireBase, the reflect will be on the frontend.
reference >>> https://firebase.google.com/docs/firestore/solutions/geoqueries
// Find cities within 50km of London
let center = CLLocationCoordinate2D(latitude: 51.5074, longitude: 0.1278)
let radiusInM: Double = 50 * 1000
// 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: radiusInM)
let queries = queryBounds.map { bound -> Query in
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 <= radiusInM {
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)
}
Thanks
If you want to listen for realtime updates, you can use .addSnapshotListener instead of getDocuments. Since this is not directly related to the geoquery, the solution documents the simpler approach - but the code for geoquerying would work exactly the same.
I tried the code below to fetch the location data filtered by nearby certain location.
but I don't know how to get documents.
FirebaseApp.configure()
let db = Firestore.firestore()
let geoFirestoreRef = db.collection("spot")
let geoFirestore = GeoFirestore(collectionRef: geoFirestoreRef)
// Query using CLLocation
let center = CLLocation(latitude: 35.681236, longitude: 139.767125)
// Query locations at [37.7832889, -122.4056973] with a radius of 100km
var circleQuery = geoFirestore.query(withCenter: center, radius: 100.0)
let _ = circleQuery.observe(.documentEntered, with: { (key, location) in
geoFirestoreRef.document(key!).getDocument { (document, error) in
if let document = document, document.exists {
print(document)
} else {
print("Document does not exist.")
}
}
print("The document with documentID '\(key)' entered the search area and is at location '\(location)'")
})
circleQuery
This variable just return GFSCircleQuery object.
I don't know how to fetch the actual documents filtered by query.
When you have a query in Firebase, you can call .getDocuments or similar. Firebase has really detailed documentation online that you should probably start with. https://firebase.google.com/docs
Problem statement:
I am currently working on an app that will track all the buses in my college. Each bus is equipped with a GPS module that sends updated coordinate along with heading to the cloud fireStore every 5 seconds. On the application side I want to read that updated coordinate whenever it changes.
Cloud firestore:
busCurrentLoc (shown in the image) has different bus numbers in it and each bus number has their respective longitude and latitude along with heading.
Application side:
The user is provided a screen where he can select which bus he wants to track. When he selects the bus number he is shown the Map screen where the bus's real time location is updated every 5 seconds. I have already done the selection process. The Real-Time tracking is the issue.
I have included necessary comments about each and every variable in the code below. The problem i am facing is updating the currentLocation variable whenever there is a change in the cloud firestore.
Code:
//The mapview outlet
#IBOutlet var mapView: MKMapView!
//The variable which stores the current coordinates of the bus from cloud firebase
var currentLocation = [String: CLLocationCoordinate2D]()
//This variable is assigned from the previous ViewController during segue.
var selectedNum: String
let db = Firestore.firestore()
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
//Function to get the coordinate from cloud firestore
getCoordinates()
}
//MARK: - Get coordinates
/**
Function to get coordinates from the Cloud firestore and store it in the global "currentLocation" variable.
*/
func getCoordinates() {
db.collection("busCurrentLoc").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
if document.documentID == self.selectedNumber {
let latitude = document.data()["latitude"]
let longitude = document.data()["longitude"]
let coord: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: latitude as! CLLocationDegrees, longitude: longitude as! CLLocationDegrees)
self.currentLocation = ["Location": coord]
// prints : ["Location": __C.CLLocationCoordinate2D(latitude: 12.89307834, longitude: 80.1411217)]
print(self.currentLocation)
self.DrawBus()
}
}
}
}
//This function should draw a bus/annotation on the currentLocation.
DrawBus(at: currentLocation)
}
//MARK: - Draw bus at currentLocation
/**
Function to Draw a bus at currentLocation.
- Parameters:
currentLocation
*/
func drawBus(coordinate at: [String: CLLocationCoordinate2D]()) {
let annotations = MKPointAnnotation()
annotations.title = "Bus Location"
annotations.coordinate = coordinate["Location"] ?? CLLocationCoordinate2D(latitude: 0, longitude: 0)
mapView.addAnnotation(annotations)
}
The annotation is added to the screen and it is displayed. But when i try changing the coordinate in the Cloud firestore the coordinate doesn't change or the annotation is not getting updated. Kindly help me solve this issue.
I managed to solve this issue by using addSnapshotListener instead of getDocument
db.collection("busCurrentLoc").addSnapshotListener{ (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
if document.documentID == self.selectedNumber {
let latitude = document.data()["latitude"]
let longitude = document.data()["longitude"]
self.currentHeading = document.data()["heading"] as! CGFloat
let coord: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: latitude as! CLLocationDegrees, longitude: longitude as! CLLocationDegrees)
self.currentLocation = ["Location": coord]
print(self.currentLocation)
self.DrawBus()
}
}
}
}
I have a class called User, which has a function that gets all nearby food trucks using GeoFire. I've used an observeReadyWithBlock to take the truck IDs returned by GeoFire, and get the rest of their information using Firebase. However, when I go to access one of the trucks from my array of Truck objects after adding their name and description, it looks like xCode is telling me the array is empty.
I am planning on using this array of nearby trucks in other controller classes, to populate tables showing all of the nearby trucks and some basic information to the user.
How can I properly populate my array of Trucks, and what could I be getting wrong based on the code below. Thanks very much!
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)
}) //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
print(nearbyTrucks[0].id)
//This line gives the error that the array index is out of range
}
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.
I want to fetch all of the location records from a user that is currently logged in.
This creates the location record on CloudKit:
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let location = locations.last!
let center = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
addCrumbPoint(center)
let locationRecord = CKRecord(recordType: "location")
locationRecord["location"] = location
let publicData = CKContainer.defaultContainer().publicCloudDatabase
publicData.saveRecord(locationRecord) { record, error in
}
}
But how would I go about pulling out the location records for only the currently logged in user?
I want to use this to create a breadcrumb of previous journeys on a map but just getting it to print a list would be a great start!
Here is my code so far:
func getLocationAsync(complete: (instance: CKRecordID?, error: NSError?) -> ()) {
let container = CKRecordID(recordName: "Location")
publicDB.fetchRecordWithID(location) { fetchedLocation, error in
if error != nil {
print(error!.localizedDescription)
complete(instance: nil, error: error)
} else {
print("fetched Location \(recordID?.recordName)")
complete(instance: recordID, error: nil)
}
}
}
If your goal is to save the breadcrumbs locations of an user and not sharing it to other users, then use the private database.
Like that you can retrieve all the location records of the user.
If you want to use the public DB, then add an entry of type CKReference to the Location record pointing to the user record. So that you can use a predicate based on the user recordID