Firestore Geohash Query with Live Updating Results in SwiftUI - swift

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)
}
}
}

Related

Swift Firebase get batches of documents in order

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.

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.

Struggling To Query Using getDocuments() in Firestore Swift

This is the first time I am using a Firestore Query and I'm struggling to parse the data. I normally use the same setup when I get documents (which works), but when I attach it to a query it does not work.
I am trying to query the database for the shop most visited, so I can later set it as favourite.
My Code:
func findFavouriteShop(completed: #escaping ([String]) -> Void)
{
// Variables
let dispatch = DispatchGroup()
var dummyDetails = [String]()
// References
let db = Firestore.firestore()
let userID = Auth.auth().currentUser?.uid
let groupCollectionRef = String("visits-" + userID! )
// Query the database for the document with the most counts
dispatch.enter()
db.collectionGroup(groupCollectionRef).order(by: "count", descending: true).limit(to: 1).getDocuments { (snapshot, error) in
if let err = error {
debugPrint("Error fetching documents: \(err)")
}
else {
print(snapshot)
guard let snap = snapshot else {return}
for document in snap.documents {
let data = document.data()
// Start Assignments
let shopName = data["shopName"] as? String
let count = data["count"] as? String
// Append the dummy array
dummyDetails.append(shopName!)
dummyDetails.append(count!)
}
dispatch.leave()
}
dispatch.notify(queue: .main, execute: {
print("USER number of documents appended: \(dummyDetails.count)")
completed(dummyDetails)}
)
}
Using Print statements it seems as if the guard statement kicks the function out. The processor does not reach the for-loop to do the assignments. When I print the snapshot it returns an empty array.
I am sure I have used the wrong notation, but I'm just not sure where.
There's a lot to comment on, such as your choice of collection groups over collections (maybe that's what you need), why you limit the results to one document but feel the need to query a collection, the naming of your collections (seems odd), the query to get multiple shops but creating a function that only returns a single shop, using a string for a count property that should probably be an integer, and using a string array to return multiple components of a single shop instead of using a custom type.
That said, I think this should get you in the right direction. I've created a custom type to show you how I'd start this process but there's a lot more work to be done to get this where you need it to be. But this is a good starting point. Also, there was no need for a dispatch group since you weren't doing any additional async work in the document parsing.
class Shop {
let name: String // constant
var count: Int // variable
init(name: String, count: Int) {
self.name = name
self.count = count
}
}
func findFavouriteShops(completion: #escaping (_ shops: [Shop]?) -> Void) {
guard let userID = Auth.auth().currentUser?.uid else {
completion(nil)
return
}
var temp = [Shop]()
Firestore.firestore().collection("visits-\(userID)").order(by: "count", descending: true).limit(to: 1).getDocuments { (snapshot, error) in
guard let snapshot = snapshot else {
if let error = error {
print(error)
}
completion(nil)
return
}
for doc in snapshot.documents {
if let name = doc.get("shopName") as? String,
let count = doc.get("count") as? String {
let shop = Shop(name: name, count: count)
temp.append(Shop)
}
}
completion(temp)
}
}
You can return a Result type in this completion handler but for this example I opted for an optional array of Shop types (just to demonstrate flexibility). If the method returns nil then there was an error, otherwise there are either shops in the array or there aren't. I also don't know if you're looking for a single shop or multiple shops because in some of your code it appeared you wanted one and in other parts of your code it appeared you wanted multiple.
findFavouriteShops { (shops) in
if let shops = shops {
if shops.isEmpty {
print("no error but no shops found")
} else {
print("shops found")
}
} else {
print("error")
}
}

Extremely confused by the scope of Firestore GetDocuments request and the in loop of Swift

All that I'm trying to do is to check whether the value for a 'key' exists in a Firestore collection as a document and return a Bool.
But I can't seem to return anything within the getDocument,
so I thought that I should then keep a results var and update the results var but the changes I make to results don't stick and it stays default false.
How do I simplify this whole mess?
func checkIfValid(db: Firestore, key: String) -> Bool {
let resolve = db.collection("keys").document(key)
var results = false
resolve.getDocument{ (document, error) in
if let document = document, document.exists {
var results = true
} else { results = false }
}
print(results)
return results
}
Reading the firebase docs, they have a small warning below the sample code.
Note: If there is no document at the location referenced by docRef,
the resulting document will be empty and calling exists on it will
return false.
However, you need to add a completion handler to the function given you're working with network requests. Swift will return the result variable given you specified it; ignoring any response from the getDocuments handler.
I changed the function to fix your mess.
func checkIfValid(db: Firestore, key: String, completion: #escaping(Bool) -> ()) {
let docRef = db.collection("user").document(key)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
print("document exists.")
completion(true)
} else {
print("document does not exists.")
completion(false)
}
}
}
And to get the response, just use this.
checkIfValid(db: db, key: "", completion: {success in
print(success)
})

How do I properly query Firestore database in swift using dispatch group

I am trying to query my firestore database and count the number of times a field is set to pre-defined enum. However, when I run it or step through it with a breakpoint, the closure never returns and my dispatch.wait() hangs forever. I am not sure why the query isn't working, as the collection exists and I have test data in there for this query. I am also able to read and write to the evaluations collection so I don't think it is a permissions issue.
I would expect at least to get an error if the query failed but it just skips over it and hangs on the wait until I stop the run.
let user = self.user
let evalRef = self.db.evaluations(forUser: user.userID)
let dispatchGroup = DispatchGroup()
for keys in self.stat.enumCases.keys {
dispatchGroup.enter()
evalRef.whereField(self.stat.queryName, isEqualTo: keys).getDocuments { (querySnapshot, err) in
if let err = err {
print(err.localizedDescription)
dispatchGroup.leave()
return
}
guard let querySnapshot = querySnapshot else {
//error
dispatchGroup.leave()
return
}
guard let enumCaseName = self.stat.enumCases[keys] else {
//error
dispatchGroup.leave()
return
}
if querySnapshot.count > 0 {
self.stat.primaryStatStruct[keys] = primaryStatForEachCase(enumCaseTitle: enumCaseName, enumCaseValue: Double(querySnapshot.count))
dispatchGroup.leave()
} else {
self.stat.primaryStatStruct[keys] = primaryStatForEachCase(enumCaseTitle: enumCaseName, enumCaseValue: 0.0)
dispatchGroup.leave()
}
}
}
dispatchGroup.wait()
Here are other snippets to give a better picture:
/// Returns a reference to the user evaluation collection.
func evaluations(forUser userID: String) -> CollectionReference {
return self.collection("users/\(userID)/evaluations")
}
print of keys and self.stat.queryName
evaluations collection with documents
document data matching keys and queryName
Any help would be greatly appreciated.