Swift Firebase get batches of documents in order - swift

For context, I have a bunch of documents that hold fields similar to a social media post. (photo url link, like count, date uploaded, person who uploaded it, etc.) And I am showing this data in a gallery (lazyvgrid). I do not want to get all of the documents at once so when the user scrolls down the gallery I am getting 20 documents at a time based on how far the user scrolls down the gallery view. I am sorting my get request with:
self.eventsDataCollection.document(currentEventID).collection("eventMedias").order(by: "savesCount", descending: true).limit(to: 20).getDocuments
I have no problem getting the first 20 using this code. How can I get the next 20 and the 20 after that, and so on?

With query cursors in Cloud Firestore, you can split data returned by a query into batches according to the parameters you define in your query.
Query cursors define the start and end points for a query, allowing you to:
Return a subset of the data.
Paginate query results.
Use the startAt() or startAfter() methods to define the start point for a query. Use the endAt() or endBefore() methods to define an endpoint for your query results.
As Dharmaraj mentioned for your case, it will be best if we use Pagination with Firestore.
Paginate queries by combining query cursors with the limit() method to limit the number of documents you would want to show in the gallery. And as you want no definite numbers, but the user should be able to scroll through as long as he wants, and as long as there are documents, I would suggest to put a cursor until the last document, like in the below code sample.
To get the last document,
let first = db.collection("collectionname")
.order(by: "fieldname")
first.addSnapshotListener { (snapshot, error) in
guard let snapshot = snapshot else {
print("Error retrieving cities: \(error.debugDescription)")
return
}
guard let lastSnapshot = snapshot.documents.last else {
// The collection is empty.
return
}

I ended up referencing Dharmaraj's link in his comment.
#Published var isFetchingMoreDocs: Bool = false
private var lastDocQuery: DocumentSnapshot!
public func getUpdatedEventMedias(currentEventID: String, eventMedias: [EventMedia], completion: #escaping (_ eventMedias: [EventMedia]) -> Void) {
self.isFetchingMoreDocs = true
var docQuery: Query!
if eventMedias.isEmpty {
docQuery = self.eventsDataCollection.document(currentEventID).collection("eventMedias").order(by: "savesCount", descending: true).limit(to: 20)
} else if let lastDocQuery = self.lastDocQuery {
docQuery = self.eventsDataCollection.document(currentEventID).collection("eventMedias").order(by: "savesCount", descending: true).limit(to: 20).start(afterDocument: lastDocQuery)
}
if let docQuery = docQuery {
print("GET DOCS")
docQuery.getDocuments { (document, error) in
if let documents = document?.documents {
var newEventMedias: [EventMedia] = []
for doc in documents {
if let media = try? doc.data(as: EventMedia.self) {
newEventMedias.append(media)
}
}
self.lastDocQuery = document?.documents.last
self.isFetchingMoreDocs = false
completion(newEventMedias)
} else if let error = error {
print("Error getting updated event media: \(error)")
self.isFetchingMoreDocs = false
completion([])
}
}
} else {
self.isFetchingMoreDocs = false
completion([])
}
}
As seen in my code, by utilizing:
.order(by: "savesCount", descending: true).limit(to: 20).start(afterDocument: lastDocQuery)
I am able to start exactly where I left off. I should also note that I am only calling this function if !isFetchingMoreDocs - otherwise the func will be called dozens of times in a matter of seconds while scrolling. The most important thing about this code is that I am checking lastDocQuery if it is nil. After the user scrolls all the way to the bottom, the lastDocQuery will no longer be valid and cause a fatal error. Also I am using a custom scroll view that tracks the scroll offset in order to fetch more media and make more calls to firebase.

Related

Firestore Geohash Query with Live Updating Results in SwiftUI

I'm trying to build an iOS app in SwiftUI where users can find a "Post" near to their current location. I have a sub collection called Posts with a geohash. Somewhat annoyingly this library by google has been archived https://github.com/firebase/geofire-objc for no reason. Instead I had to use this library https://github.com/emilioschepis/swift-geohash.
I find all the neighboring geohashes around the current user and then run a query against firstore for each geohash starting with geohash and ending with geohash + '~'.
Here is the function I wrote:
// import https://github.com/emilioschepis/swift-geohash
class FirestorePosts: ObservableObject {
#Published var items = [FirestorePost]() // Reference to our Model
func geoPointQuery(tag:String){
do {
let db = Firestore.firestore().collection("tags")
let docRef = db.document(tag).collection("posts")
// users current location is "gcpu"
let neighbors = try Geohash.neighbors(of: "gcpu", includingCenter: true)
let queries = neighbors.map { bound -> Query in
let end = "\(bound)~"
return docRef
.order(by: "geohash")
.start(at: [bound])
.end(at: [end])
}
func getDocumentsCompletion(snapshot: QuerySnapshot?, error: Error?) -> () {
guard let documents = snapshot?.documents else {
print("Unable to fetch snapshot data. \(String(describing: error))")
return
}
self.items += documents.compactMap { queryDocumentSnapshot -> FirestorePost? in
return try? queryDocumentSnapshot.data(as: FirestorePost.self)
}
}
for query in queries {
print("ran geo query")
query.getDocuments(completion: getDocumentsCompletion)
}
}
catch{
print(error.localizedDescription)
}
}
}
So far the query works and returns items as expected. However, the results are not updated in realtime when there is a change in Firestore.
How could I make this query update results in realtime? I tried adding query.addSnapshotListener instead but it doesn't like "completion:" parameter
How can I ensure that all the queries are finished before returning the results
You're calling query.getDocuments, which gets data once. If you want to also get updates to that data, you should use addSnapshotListener which listens for updates after getting the initial docs.
To ensure all queries are finished, you could keep a simple counter that you increase each time your addSnapshotListener callback is invoked. When the counter is equal to the number of queries, all of them have gotten a response from the server. That's exactly what the geofire-* libraries for Realtime Database do for their onReady event.
I refactored to this and it seems to work and updates in realtime. I didn't need to use a counter since Im appending the documents to self.items (not sure if thats correct though).
...
for query in queries {
query.addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.items += documents.compactMap { queryDocumentSnapshot -> FirestorePost? in
return try? queryDocumentSnapshot.data(as: FirestorePost.self)
}
}
}

How to pull all product data from index in Algolia?

After reading the docs on how to search and browse an index with Algolia's Swift Client, it's not clear how I need to pull all product data from an index. In the documentation, it is stated that:
The search query only allows for the retrieval of up to 1000 hits. If
you need to retrieve more than 1000 hits (e.g. for SEO), you can
either leverage the Browse index method or increase the
paginationLimitedTo parameter
So I wrote the following:
let client = SearchClient(appID: "...", apiKey: "...")
var index: Index
index = client.index(withName: "products")
var productFeed:[Product] = []
let settings = Settings()
.set(\.paginationLimitedTo, to: 4500)
index.setSettings(settings) { result in
if case .success(let response) = result {
.....
}
}
Then to Browse:
index.browse(query: Query("")) { result in
if case .success(let response) = result {
do {
let products:[Product] = try response.extractHits()
DispatchQueue.main.async {
self.productFeed = products
}
}catch let error{
print("Hits decoding error :\(error)")
}
}
}
It would seem as though the two blocks of code would work together, but my productFeed array just returns 1000 records. Can someone explain what I am doing wrong here?
To retrieve all records from your index use the browseObjects method.
This method performs multiple consecutive browse method calls extracting all records from an index page by page.

How to store a full Firestore query path?

After doing a query to Firestore i'm attempting to store the Firestore path so that i can acces it later.
This way i can do a Firestore query and attach a listener. On the second query i can access that stored path to detach the old listener and attach a new one.
When initializing a app a query is executed.
override func viewDidLoad() {
super.viewDidLoad()
let path = Firestore.firestore().collection("all_users").document(uid).collection("usercollection").whereField("datefilter", isGreaterThan: self.currentDate)
newSearch(firebasePath: path)
However, storing the path is necessary as the query can change in unpredictable ways. Below is a function that changes the query depending on user input.
func filterQuery(filterText:string) {
let path = Firestore.firestore().collection("all_users").document(uid).collection(filterText).whereField("datefilter", isGreaterThan: self.currentDate)
newSearch(firebasePath: path)
A query is made and a listener is attached on that specific query. This way i can grab the data and listen when something is modified to that specific query.
func newSearch(firebasePath: Query) {
//REMOVE OLD LISTENER IF IT EXISTS OR IF QUERY IS THE SAME ---> ADD NEW ONE AT THE END OF FUNCTION
if self.storedFirebasePath != nil {
if self.storedFirebasePath != firebasePath {
let listener =
self.storedFirebasePath.addSnapshotListener { snapshot, error in
}
listener.remove()
}
}
let queryRef = firebasePath
queryRef.getDocuments() { (querySnapshot, err) in
if err == nil && querySnapshot != nil {
if querySnapshot!.documents.count > 0 {
self.mapIsEmpty = false
for document in querySnapshot!.documents {
let data = document.data()
//MANAGING DATA....
}
} else {
print("NO DATA")
}
}
}
}
//STORE FIRESTORE PATH
self.storedFirebasePath = queryRef
//ATTACH LISTENER
self.realTimeUpdates(firebasePath: firebasePath)
}
}
With code below i get "Currently i get the Error = 'init()' is unavailable: FIRQuery cannot be created directly"
var storedFirebasePath = Query()
Maybe the parameters used to create the query can be stored to recreate the path. Or is there a better practice to do what i'm attempting
I don't think what you're trying to do is possible. You can however store a document reference. But even with a document reference you can't detach listeners with it.

Firestore SnapshotListener stops updating data when using multiple criteria

I have a repository in my SwiftUI app that grabs all actions for a particular user from Google Cloud Firestore. It uses a Snapshot Listener to keep the action list up to date.
I wanted to change the query to limit results to actions that weren't already completed, by adding this line to my code:
.whereField("completed", isEqualTo: false)
And here is the function it was added to, that creates the listener and sets it to the "actions" variable in the repository:
func loadData() {
let userId = Auth.auth().currentUser?.uid
if userId != nil {
self.listener = db.collection("action")
.order(by: "createdTime")
.whereField("userId", isEqualTo: userId!)
.whereField("completed", isEqualTo: false)
.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.actions = querySnapshot.documents.compactMap { document in
do {
let x = try document.data(as: Action.self)
return x
}
catch {
print(error)
}
return nil
}
}
}
} else {
return
}
}
But once I added that (I have an index for userId, but not for completed, if that matters), it loads data correctly the first time, but the view does not get updated when I add or change existing actions, until I quit and reopen the app.
Is there any issue with having multiple criteria like this for a Snapshot listener? Or anything else that would be causing this listener to not get dynamic updates any more?
You need to create composite indexes for multiple queries
How to create compound queries
You can read the compound queries section of firebase documentation.
Creating compund queries requires some time..

Swift Firestore query with filter for documents near me

I have an iOS app that has employees that need to be notified of pickups nearby. I want to perform a Firestore query and filter the radius of documents returned. I came up with this solution
but #Frank van Puffelen pointed out my question may be an x y problem and he was right!
I want is to listen to firebase Firestore for documents added then return results near my location. My documents have geopoint and geohash data. Is there a way to write a query in swift that has a listener attached and filters for documents within 100 miles? This is my current query in swift:
extension Firestore {
var pickups: Query {
return collection("pickups").whereField("employeeUID", isEqualTo: "none").order(by:
"date")
}
}
func getPickups() {
listener = db.pickups.addSnapshotListener({ (snap, error) in
guard let documents = snap?.documents else { return }
for document in documents {
let data = document.data()
let newPickup = PickUps(data: data)
self.pickups.append(newPickup)
}
})
}
Any ideas about a query would be greatly appreciated - Thank you!