Can I apply pagination on GeoFire Query to reduce load time - ios

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

Related

Retrieving city name for onscreen region [Mapbox]

I'm relatively new to iOS and Mapbox development. I'm working on an app where a user can freely manipulate a map full of places they have saved.
When they reach a zoom-level that is completely filled by the geography of a city, I would like to display the name of the city which they are viewing in a banner-style view, even if a city label is not within view on the map (as is often the case when zoomed in).
Here's a screenshot of the UI for context.
I'm trying to query the Mapbox tileset for the city name using the following code:
func mapViewRegionIsChanging(_ mapView: MGLMapView) {
let zoomLevel = mapView.zoomLevel
if zoomLevel >= 14.0 {
// layer identifier taken from layer name in Mapbox Studio
let layerIdentifier = "place-city-lg-n"
let screenRect = UIScreen.main.bounds
let cityName = mapView.visibleFeatures(in: screenRect, styleLayerIdentifiers: Set([layerIdentifier]))
print(cityName)
}
I think this code doesn't work because the label is not onscreen at the specified zoom level.
I'm wondering if using visibleFeaturesInRect is the best approach for my need—is there a better way to retrieve city name regardless of visible elements and zoom level?
For this task I'd recommend using MapboxGeocoder from Mapbox. It is for getting information about the city/village.
you can install pod:
pod 'MapboxGeocoder.swift', '~> 0.12'
and use this code:
let geocoder = Geocoder.shared
func mapViewRegionIsChanging(_ mapView: MGLMapView) {
let geocodeOptions = ReverseGeocodeOptions(coordinate: mapView.centerCoordinate)
geocodeOptions.allowedScopes = [.place]
let _ = geocoder.geocode(geocodeOptions) { (placemarks, attribution, error) in
guard let placemark = placemarks?.first else { return }
print(placemark.name)
print(placemark.qualifiedName)
}
}
you can add your conditions and it really helps to solve your task

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.

Receive limited query with custom start point

I'm building a basic chat app with swift for iOS with firebase realtime database.
The Messages are observed with a limit for the least 10.
Now, I want to implement the functionality of loading earlier send messages. Currently I'm trying to achieve this by using this function:
let query = threadRef.child("messages").queryOrderedByKey().queryStarting(atValue: "2").queryLimited(toLast: 2)
Which returns this query:
(/vYhNJ3nNQlSEEXWaJAtPLhikIZi1/messages {
i = ".key";
l = 2;
sp = 2;
vf = r;
})
And this should give me the data:
query.observeSingleEvent(of: .value, with: { (snap) in
But it just limits the query and not set the start point to a specific position.
Here is the firebase database structure:
messages
-Kgzb3_b26CnkTDglNd8
date:
senderId:
senderName:
text:
-Kgzb4Qip6_jQdKRWFey
-Kgzb4ha0KZkLZeBIaxW
-Kgzb577KlNKOHxsQo9W
-Kgzb5cqIVMhRmU019Jf
Anyone have an idea on how to implement a feature like that?
Okay I finally found a way to do what I wanted.
First of all I misunderstood the way to access data from Firebase.
This is now how I get the query:
let indexValue = messages.first?.fireBaseKey
let query = messageRef.queryOrderedByKey().queryEnding(atValue:indexValue).queryLimited(toLast: 3)
1) get the FireBase key I previously saved to my custom chat messages
2) construct the query:
order it by key
set the ending to oldest message
limit the array to query to desired length
Then to actually get the query I used:
query.observeSingleEvent(of: .value, with: { snapshot in
for child in snapshot.children.dropLast().reversed() {
let fireSnap = (child as! FIRDataSnapshot)
//do stuff with data
}
})
1) get the query as a single event
2) iterate over children and I needed to dropLast() to make sure I don't have any duplicated messages and reverse it to get the correct order.
3) cast the current child as a FIRDataSnapshot to access the data
Since I couldn't find a simple example for this so I thought I leave my solution here incase other people running into the same problem.

How do I use a firebase server timestamp, with queryStartingAtValue, to track a user's current actions, for a firebase listener in iOS & Swift

I am developing an iOS app in Swift which has a like feature which is the same concept as liking a Facebook post, a Twitter Tweet etc.
I want to create a listener that only listens to the user liking posts with a timestamp that starts from now. i.e once they open the app. I wanted to use a firebase server timestamp value to get the current timestamp
I have the following structure in my database
"userLikes": {
"$uid":{
"$messageId": {
"timestamp": 1212121212121 // timestamp created using fb server
}
}
}
This was my attempted solution but it doesn't work and the problem I have is to do with -> FIRServerValue.timestamp()
let queryRef = ref.child("userLikes").child(uid).queryOrdered(byChild: "timestamp").queryStarting(atValue: FIRServerValue.timestamp())
queryRef.observe(.childAdded, with: { (snapshot) in
let utterId = snapshot.key
self.newsFeedModel.uttersInfo[utterId]?[Constants.UttersInfoKeys.isLikedByCurrentUser] = true as AnyObject
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}, withCancel: nil)
Any ideas how I Can implement this? Perhaps I should just use NSDate to get the current time for comparison but thought that using a firebase server timestamp would be the optimum way for comparison purposes.
You can estimate the ServerTime by taking the local time and correcting for the clock-skew and latency. The Firebase documentation has a section on Clock Skew.
Clock Skew
While firebase.database.ServerValue.TIMESTAMP is much more accurate, and preferable for most read/write ops, it can occasionally be useful to estimate the clients clock skew with respect to the Firebase Realtime Database's servers. We can attach a callback to the location /.info/serverTimeOffset to obtain the value, in milliseconds, that Firebase Realtime Database clients will add to the local reported time (epoch time in milliseconds) to estimate the server time. Note that this offset's accuracy can be affected by networking latency, and so is useful primarily for discovering large (> 1 second) discrepancies in clock time.
let offsetRef = FIRDatabase.database().referenceWithPath(".info/serverTimeOffset")
offsetRef.observeEventType(.Value, withBlock: { snapshot in
if let offset = snapshot.value as? Double {
let estimatedServerTimeMs = NSDate().timeIntervalSince1970 * 1000.0 + offset
}
})
This will likely work better than purely using a client-side timestamp, since that is likely to be different between clients.
Update for Swift 3/4:
public func getCurrentTime(completionHandler:#escaping (Double) -> ()){
Database.database().reference(withPath: ".info/serverTimeOffset").observeSingleEvent(of: .value) { (snapshot) in
if let time = snapshot.value as? Double{
completionHandler(Date().timeIntervalSince1970 + time)
}else{
completionHandler(0)
}
}
}
Usage:
getCurrentTime(){ (date) in
print(date)
}

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

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

Resources