How do I properly use a stored search radius with GeoFire? - ios

I'm using GeoFire to search a given radius, which the user can set in my app and store on FireBase. When the page loads, before running the GeoFire query I am getting the radius from Firebase. However, when I run the code below, I get the following error: Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Precision must be less than 23!'
From playing around with some strategically placed print statements, it looks like the searchRadius is returning as 0 by the time the GeoFire query runs, leading me to suspect that asynchronous loading is at play.
My question is, am I getting this error due to my searchRadius being 0, and if so how can I ensure that the FireBase block that grabs my user's search radius runs before my GeoFire query?
self.ref.childByAppendingPath("users/\(self.ref.authData.uid)/searchRadius").observeEventType(.Value, withBlock: { snapshot in
self.searchRadius = snapshot.value as! Double
})
let center = CLLocation(latitude: 37.331469, longitude: -122.029825)
let circleQuery = geoFire.queryAtLocation(center, withRadius: self.searchRadius)
circleQuery.observeEventType(GFEventTypeKeyEntered, withBlock: { (key: String!, location: CLLocation!) in
//Do code for each result returned
}) //End GeoFire query

Yes, this is an async loading issue. Even if Firebase has already cached the value of searchRadius the value callback will not be executed inline. The thread will go on set up the GeoFire query before the value is updated.
Based on this code snippet, you can ensure that the radius is set before running the query by moving the query code inside of the callback. You may also want to observe a single event so that the query isn't run again if the value of searchRadius changes.
self.ref.childByAppendingPath("users/\(self.ref.authData.uid)/searchRadius").observeSingleEventOfType(.Value, withBlock: { snapshot in
self.searchRadius = snapshot.value as! Double
let center = CLLocation(latitude: 37.331469, longitude: -122.029825)
let circleQuery = geoFire.queryAtLocation(center, withRadius: self.searchRadius)
circleQuery.observeEventType(GFEventTypeKeyEntered, withBlock: { (key: String!, location: CLLocation!) in
//Do code for each result returned
}) //End GeoFire query
}) // End observeSingleEventOfType

Related

iOS callback if firebase query doesn't run (Swift Firebase)

I have a query running to check .childAdded at a location in my database.
It works well when data is found, however, if there is not data at the location, it can't fire the query and therefore this does not allow me to use snapshot.exists because it doesn't even run the query.
This is my current code
let favouriteRef = self.databaseRef.child("users").child(userID!).child("Favourites")
// Code doesn't run past this line when no data at location
favouriteRef.queryOrderedByKey().observe(.childAdded, with: { (snapshot) in
let favouriteID = "\(snapshot.value!)"
let usersRef = self.databaseRef.child("users")
usersRef.observeSingleEvent(of: .value, with: { (users) in
for user in users.children {
let favCleaner = UserClass(snapshot: user as! DataSnapshot)
if favouriteID == favCleaner.uid {
tempFav.append(favCleaner)
}
}
self.hasFavourites = true
self.tableView.reloadData()
self.usersArray = tempFav
self.collectionView.reloadData()
})
})
I would like to find a way to receive a callback if the query doesn't run (no data at location)
Thanks.
If there is absolutely no data at the location, then obviously this event trigger is not sufficient for you to get the data because this event only gets triggered when something gets added.
So you have two options
Add another trigger of type .observeSingleEvent(of: .value, with: { (snapshot) in
// Get the values and write the relevant actions
}
Or you can update this event to type .observe(.value, with: {snapshot in // write the relevant code
}
Both approaches have their advantage and disadvantage.
First approach will need you to write more code while minimising the number of triggers from your database to your UI. In second approach, there will be more triggers to the UI but it can be fairly easy to code.
From what I can see, you are first trying to establish whether data exists in the favorite node of your database and then comparing it to another snapshot. So if the number of delete or update events are relatively small, my suggestion is to go for approach two.

Can I apply pagination on GeoFire Query to reduce load time

I have used GeoFire to fetch location based data from my Firebase database. I know if I set less radius in my query then I am able to load data quickly, but my requirement is that I want shorted data based on location, so nearest records shows first, and so on. So I have passed current location in GeoFire query with total earth radius, because I want all data. But I don't how to apply pagination with GeoFire, so in future when more records are available in Firebase database my current implementation will definitely takes more time to load.
Below is the code snipped which I have used to get location based records.
let eartchRadiusInKms = 6371.0
let geoFire = GeoFire(firebaseRef: databaseRef.child("items_location"))
let center = CLLocation(latitude: (self.location?.coordinate.latitude)!, longitude: (self.location?.coordinate.longitude)!)
let circleQuery = geoFire?.query(at: center, withRadius: eartchRadiusInKms)
if CLLocationCoordinate2DIsValid(center.coordinate) {
circleQuery?.observeReady({
let myPostHandle : DatabaseHandle = circleQuery?.observe(.keyEntered, with: { (key: String?, location: CLLocation?) in
// Load the item for the key
let itemsRef = self.databaseRef.child("items").child(key!)
itemsRef.observeSingleEvent(of: .value, with: { (snapshot) in
// Manage Items data
})
})
})
}
So can pagination is possible with GeoFire? Or I have to use some different mechanism, can anyone please advise me on this scenario?
I faced a similar issue and I actually loaded a small radius first, then increased the radius and loaded another chunk of data in a background service. In the completion of the call I reloaded my data in collection view using
collectionView.reloadData()`
Here is how you query in geofire
self.circleQuery = self.geoFire?.query(at: myLocation, withRadius: myRadius)
self.circleQuery?.observe(.keyEntered, with: { (key: String?, location: CLLocation?) in ....
check out this function in geofire documentation. It keeps a track of the new entered locations in background. Now as you want pagination consider an example of tableView, you can just call this on onScroll
myRadius = myRadius + 1000 // or any number to increase radius
As the keyEntered observer is already set so it will return you back the new results. Just add them to your list and update the table / collection view

Google Places iOS: How to provide context to API for relevant local results

Using Google Places API for iOS, I am using the Place AutoComplete feature. When I start typing a place name for example 'Starbucks' it gives me starbucks location in several countries but not my local starbucks. How do I pass the current location to the API so the search results start from closest to my location?
You can pass a GMSCoordinateBounds to the which is "object biasing the results to a specific area specified by latitude and longitude bounds".
They have an example on their site showing how to do this:
let visibleRegion = self.mapView.projection.visibleRegion()
let bounds = GMSCoordinateBounds(coordinate: visibleRegion.farLeft, coordinate: visibleRegion.nearRight)
let filter = GMSAutocompleteFilter()
filter.type = GMSPlacesAutocompleteTypeFilter.City
placesClient.autocompleteQuery("Sydney Oper", bounds: bounds, filter: filter, callback: { (results, error: NSError?) -> Void in
guard error == nil else {
print("Autocomplete error \(error)")
return
}
for result in results! {
print("Result \(result.attributedFullText) with placeID \(result.placeID)")
}
})
This example show how to pull this data from a map view but you can also create the GMSCoordinateBounds manually.
Hope this helps.

Convert asynchronously large no of GPS data to city/country

Background: I have a list of contacts, which are retrieved on an asynchronous queue from a cloud based database. Once done, I dispatch back to the main queue and show these contacts in a TableView.
Besides names and other details, each contact object has GPS coordinate properties (latitude and longitude). I want to use these GPS coordinates to retrieve the name of the city and country of each contact and update the TableView showing that information in the local language of the device the user has.
Problem: The problem I am trying to overcome is that I have a few hundred contacts. Initially, I used concurrent queues to get the city/country strings. But I paused the app in XCode and realised the disaster of 300+ threads created. So I changed the code running each lookup of the city/country on the same serial queue.
The issue now is that only for about 50 of the contacts the data is updated. I do not know why and not sure even if the queue is serial. Debugging shows there are still 100+ “serial queues” created. I expected one at the time. What am I doing wrong? Thanks
The code for the class, in which I have my TableView is as follows:
dispatch_async(dispatch_get_global_queue(Int(QOS_CLASS_USER_INITIATED.rawValue), 0)) {
// get or update the contactsList
activeUser.contactsList.getAllContacts()
// once we have the contacts we go back to the main queue
dispatch_async(dispatch_get_main_queue()) {
// and refresh tableview to show the contacts
self.tableView.reloadData()
// now we refresh the locations of the contacts
for index in 0...activeUser.contactsList.listOfContacts.count
{
// set local variables for lat and long
let lat = activeUser.contactsList.listOfContacts[index].latitude
let long = activeUser.contactsList.listOfContacts[index].longitude
// call location service method with completion handler
MyLocationServices().updateLocationToLocalLanguage(lat, longitude: long, completionHandler:
{ (city, country) -> () in
// update contact's details
activeUser.contactsList.listOfContacts[index].city = city
activeUser.contactsList.listOfContacts[index].country = country
// refresh each time the table to show updated contact data
self.tableView.reloadData()
})
}
}
The method within the MyLocationServices class looks as follows:
func updateLocationToLocalLanguageDispatched(latitude: String, longitude: String, completionHandler: (city: String, country: String) -> ())
{
let serialQ = dispatch_queue_create("AddressUpdateQ", DISPATCH_QUEUE_SERIAL)
dispatch_async(serialQ)
{
var cityString = "NA"
var countryString = "NA"
let group = dispatch_group_create()
dispatch_group_enter(group)
let location = CLLocation(latitude: Double(latitude)!, longitude: Double(longitude)!)
self.geocoder.reverseGeocodeLocation(location)
{
(placemarks, error) -> Void in
if let placemarks = (placemarks as [CLPlacemark]!) where placemarks.count > 0
{
let placemark = placemarks[0]
if ((placemark.addressDictionary!["City"]) as? String != nil) { cityString = ((placemark.addressDictionary!["City"]) as? String)! }
if ((placemark.addressDictionary!["Country"]) as? String != nil) { countryString = ((placemark.addressDictionary!["Country"]) as? String)! }
}
dispatch_group_leave(group)
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
dispatch_async(dispatch_get_main_queue())
{
completionHandler(city: cityString, country: countryString)
}
}
}
I have solved the issue and documenting hoping it is helpful in case others face similar problems.
(1) Serial Queue: I have realised serial queue initiation should not happen within the method, but enclose the for-loop. Thus the following code should be removed from the method and put around the for-loop. But this does not solve the fact that I only get about 50 results and nothing for the remaining GPS data of the contacts.
let serialQ = dispatch_queue_create("AddressUpdateQ", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQ)
{ [for index....] }
(2) Location: The second issue is related to Apple's Geocoder, which happens online. Officially, the number of requests are limited. The documentation states that
one "should not send more than one geocoding request per minute".
Applications should be conscious of how they use geocoding. Geocoding
requests are rate-limited for each app, so making too many requests in
a short period of time may cause some of the requests to fail.
Checking the error code I get, it is Error Domain=kCLErrorDomain Code=2. It seems the service is denied and I cannot make further requests.
(3) Solutions: There are two solutions in my view. One is to update 30 or so users, wait some time, and then dispatch after that another queue to get again some data. The second solution is to live with having all data in English, make a string that each user saves in the cloud. So for other users retrieving contacts, it's just a matter of fetching a string from the database rather than checking GPS data "on the fly".

Sending and receiving push notification swift

I created a small messenger app using Parse as my database in swift. I wanted each time any user sends a message only other users within 10 meter to get the notification. The app is working but the push notification is not. I did some research it looks like my code is similar to the one i found but still not working. Please help me. Thank you
var CurrentLocation : PFGeoPoint = PFGeoPoint(latitude: 44.6854, longitude: -73.873) // assume the current user is here
let userQuery = PFUser.query()
userQuery?.whereKey("Location", nearGeoPoint: CurrentLocation, withinMiles: 10.0)
let pushQuery = PFInstallation.query()
pushQuery?.whereKey("username", matchesQuery: userQuery!)
let push = PFPush()
push.setQuery(pushQuery)
push.setMessage(" New message")
push.sendPushInBackground()
Your problem is the line pushQuery?.whereKey("username", matchesQuery: userQuery!). According to the parse docs Warning: This only works where the key’s values are PFObjects or arrays of PFObjects. Read more here: https://parse.com/docs/osx/api/Classes/PFQuery.html#//api/name/whereKey:matchesQuery
Instead change it to chain the query by first performing the first query and then using the strings of the userIds for the second query which is performed inside the first's completionBlock.
Update
Also, just as a side note you are not abiding by the camel case rules of swift and also the rules set out by Parse. You should follow conventions. (See my code for the correct case for keys in Parse and variable names).
Example:
var currentLocation : PFGeoPoint = PFGeoPoint(latitude: 44.6854, longitude: -73.873) // assume the current user is here
let userQuery = PFUser.query()
userQuery?.whereKey("location", nearGeoPoint: currentLocation, withinMiles: 10.0) // Note I changed Location to location
userQuery?.findObjectsInBackground({ results, error in
let usernames = (results as! [PFObject]).map { $0.username }
let pushQuery = PFInstallation.query()
pushQuery?.whereKey("username", containedIn: usernames)
let push = PFPush()
push.setQuery(pushQuery)
push.setMessage("New message")
push.sendPushInBackground()
})
Also note that you may need to change your structure because the docs for PFInstallation.query note that you must use one of three query parameters and you are using none (you might have to save the installation object id to a field on the user. Then instead of making an array with username make an array with the installation object ids and query PFInstallation like that. However, this may still work so try it first, you never know.

Resources