Struggling To Query Using getDocuments() in Firestore Swift - 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")
}
}

Related

swift for loop order of data is not right

I want to fetch data from firebase and put them in an array. The first part of the function is always in the right order, i can see it when i print(DEBUG(files). But after for loop, the order of the documents messes and i always get random order. Shouldn't i always get the same order?
func getUnreadMessages(){
guard let uid = AuthViewModel.shared.userSession?.uid else {return}
Firestore.firestore().collection("users").document(uid).collection("chats").order(by: "created", descending: true).getDocuments { (snapshot, _) in
guard let files = snapshot?.documents.compactMap({ $0.documentID }) else {return}
print("DEBUG: \(files)")
for file in files{
Firestore.firestore().collection("users").document(uid).collection("chats").document(file).collection("messages").whereField("read", isEqualTo: false).getDocuments { (snapshot, _) in
guard let documents = snapshot?.documents.compactMap({ $0.documentID }) else {return}
print("DEBUG: \(documents)")
self.count.append(documents.count)
print("DEBUG: \(self.count)")
}
}
}
}
You get a different order of results because while you call the database in the correct order, there is no guarantee that the database will return your call in that same order, because some calls take longer than other calls. I think the simplest solution is to record the original order, attach it to the data in your second call (where you determine document count), and sort the collection (the array) by that original order.
The easiest way to attach this index value to the document count is a custom model:
struct MessageCount {
let count: Int // this is the message count you're after
let n: Int // this is the index of the original order
init(count: Int, n: Int) {
self.count = count
self.n = n
}
}
Then just use a dispatch group to coordinate the async tasks and in the completion of the dispatch group, sort the array by index and you will have an array of message counts in the intended order:
func getUnreadMessages() {
guard let uid = AuthViewModel.shared.userSession?.uid else {
return
}
let db = Firestore.firestore() // instantiate it once since it could be created hundreds or thousands of times in this function
db.collection("users").document(uid).collection("chats").order(by: "created", descending: true).getDocuments { (snapshot, error) in
guard let snapshot = snapshot,
!snapshot.isEmpty else {
if let error = error {
print(error) // you oddly omitted the error in your code, never do that
}
return
}
let dispatch = DispatchGroup() // set up the dispatch group outside the loop
var messageCounts = [MessageCount]() // this temp array will carry the data with the index
// to record the original order of the loop, just enumerate it and access `n` (the index)
for (n, doc) in snapshot.documents.enumerated() {
dispatch.enter() // enter dispatch on each iteration
db.collection("users").document(uid).collection("chats").document(doc.documentID).collection("messages").whereField("read", isEqualTo: false).getDocuments { (snapshot, error) in
if let snapshot = snapshot {
let c = snapshot.count // get the message count
let count = MessageCount(count: c, n: n) // add it to the model along with n which is captured by the parent closure
messageCounts.append(count) // append to our temp array
} else if let error = error {
print(error)
}
dispatch.leave() // leave dispatch no matter the outcome
}
}
// this is the completion handler of the dispatch group
dispatch.notify(queue: .main) {
// sort the array by index and then map it to just get the message counts
let counts = messageCounts.sorted(by: { $0.n < $1.n }).map({ $0.count })
}
}
}
The order of returned results are determined by an order(by clause. Otherwise the results may seem somewhat random.
In this case the first Firebase call specifies an order, so those documents will always be returned in the correct order.
collection("chats").order(by: "created"
But the next firebase call does not specify an order so, the returned documents may be somewhat inconsistently ordered.
.collection("messages").whereField
We need to have some way to guarantee that order.
Suppose the structure is this
chats (collection)
user ids (documents)
chats (collection)
chat ids (documents)
messages (collection)
message ids (documents that you want ordered)
the message id's would need to have a field to order them by - call that ordering
Here's the code that prints the count of the number of messages in each chat id and then prints the messages in order
func getUnreadMessages() {
let uid = "uid_0"
self.db.collection("users_chats").document(uid).collection("chats").getDocuments(completion: { snapshot, error in
if let err = error {
print(err.localizedDescription)
return
}
guard let docs = snapshot?.documents else { return }
for doc in docs {
let ref = doc.reference.collection("messages")
ref.order(by: "ordering").getDocuments(completion: { messagesSnapshot, error in
if let err = error {
print(err.localizedDescription)
return
}
guard let messages = messagesSnapshot?.documents else { return }
print("the chat document: \(doc.documentID) has \(messages.count) messages")
for msg in messages {
let order = msg.get("ordering")
let msg = msg.get("read")
print("order: \(order!)", "is read: \(msg!)")
}
})
}
})
}
if there were three messages in chat 0, the output looks like this
the chat document: chat_0 has 3 messages
the chat document: chat_1 has 0 messages
the chat document: chat_2 has 0 messages
order: 0 isRead: 0
order: 1 isRead: 1
order: 2 isRead: 0

How to merge two queries using Firestore - Swift

I need to merge two queries with firebase firestore and then order the results using the timestamp field of the documents.
Online I didn't find much information regarding Swift and Firestore.
This is what I did so far:
db.collection("Notes").whereField("fromUid", isEqualTo: currentUserUid as Any).whereField("toUid", isEqualTo: chatUserUid as Any).getDocuments { (snapshot, error) in
if let error = error {
print(error.localizedDescription)
return
}
db.collection("Notes").whereField("fromUid", isEqualTo: self.chatUserUid as Any).whereField("toUid", isEqualTo: self.currentUserUid as Any).getDocuments { (snaphot1, error1) in
if let err = error1{
print(err.localizedDescription)
return
}
}
}
I added the second query inside the first one on completion but now I don't know how to merge them and order them through the field of timestamp.
On this insightful question It is explained that it's recommended to use a Task object but I don't find anything similar with swift.
There are many ways to accomplish this; here's one option.
To provide an answer, we have to make a couple of additions; first, we need somewhere to store the data retrieved from firebase so here's a class to contains some chat information
class ChatClass {
var from = ""
var to = ""
var msg = ""
var timestamp = 0
convenience init(withDoc: DocumentSnapshot) {
self.init()
self.from = withDoc.get("from") as! String
self.to = withDoc.get("to") as! String
self.msg = withDoc.get("msg") as! String
self.timestamp = withDoc.get("timestamp") as! Int
}
}
then we need a class level array to store it so we can use it later - perhaps as a tableView dataSource
class ViewController: NSViewController {
var sortedChatArray = [ChatClass]()
The setup is we have two users, Jay and Cindy and we want to retrieve all of the chats between them and sort by timestamp (just an Int in this case).
Here's the code that reads in all of the chats from one user to another creates ChatClass objects and adds them to an array. When complete that array is passed back to the calling completion handler for further processing.
func chatQuery(from: String, to: String, completion: #escaping( [ChatClass] ) -> Void) {
let chatsColl = self.db.collection("chats") //self.db points to my Firestore
chatsColl.whereField("from", isEqualTo: from).whereField("to", isEqualTo: to).getDocuments(completion: { snapshot, error in
if let err = error {
print(err.localizedDescription)
return
}
guard let docs = snapshot?.documents else { return }
var chatArray = [ChatClass]()
for doc in docs {
let chat = ChatClass(withDoc: doc)
chatArray.append(chat)
}
completion(chatArray)
})
}
Then the tricky bit. The code calls the above code which returns an array The above code is called again, returning another array. The arrays are combined, sorted and printed to console.
func buildChatArray() {
self.chatQuery(from: "Jay", to: "Cindy", completion: { jayCindyArray in
self.chatQuery(from: "Cindy", to: "Jay", completion: { cindyJayArray in
let unsortedArray = jayCindyArray + cindyJayArray
self.sortedChatArray = unsortedArray.sorted(by: { $0.timestamp < $1.timestamp })
for chat in self.sortedChatArray {
print(chat.timestamp, chat.from, chat.to, chat.msg)
}
})
})
}
and the output
ts: 2 from: Cindy to: Jay msg: Hey Jay, Sup.
ts: 3 from: Jay to: Cindy msg: Hi Cindy. Not much
ts: 9 from: Jay to: Cindy msg: Talk to you later

How can I add these Firestore fields to a Dictionary?

I am looking to add all my "usernames" into a dictionary. I am having some trouble doing this. I am sure it's very obvious, but I am very new to coding.
I am stuck at, right now and can't seem to find a clear answer anywhere:
func fetchUser() {
let db = Firestore.firestore()
let usernameSearch = db.collection("users")
usernameSearch.getDocuments { (snapshot, error) in
if error != nil {
print("Error obtaining usernames")
} else {
for field in snapshot!.documents {
let field = field.get("username")
print(field!)
}
}
}
}
I would really appreciate it if somebody could help me out. I am sure it's very obvious, or I'm just doing it totally wrong.
First, get into the habit of safely unwrapping over force unwrapping. And choose more accurate names for your objects (i.e. usersCollection over usernameSearch). However, in this case, there's no need to instantiate individual properties for the database and the collection since they're not being used anywhere else but here (so be efficient and omit them).
var usersDictionary = [String: [String]]()
func fetchUser() {
Firestore.firestore().collection("users").getDocuments { (snapshot, error) in
if let snapshot = snapshot { // unwrap the snapshot safely
var usernames = [String]()
for doc in snapshot.documents {
if let username = doc.get("username") as? String {
usernames.append(username)
}
}
usersDictionary["usernames"] = usernames
} else {
if let error = error {
print(error)
}
}
}
}
Or if you actually meant an array of users:
var usersArray = [String]()
func fetchUser() {
Firestore.firestore().collection("users").getDocuments { (snapshot, error) in
if let snapshot = snapshot { // don't force unwrap with !
for doc in snapshot.documents {
if let username = doc.get("username") as? String {
usersArray.append(username)
}
}
} else {
if let error = error {
print(error)
}
}
}
}
I'm assuming that what you're looking for is an Array, not a Dictionary. I'll also assume that you are indeed getting the correct value that you'd expect out of field.get("username"), e.g. a string such as "Bob." Therefore, what you are trying to do is map the list of document objects to a list of strings.
If you scroll to the Topics section of the Array documentation from Apple, you can find some of the operations they provide for arrays such as snapshot!.documents.
One of those operations is actually map, and its description is:
Returns an array containing the results of mapping the given closure over the sequence’s elements.
https://developer.apple.com/documentation/swift/array/3017522-map
In other words, you provide a transformation to perform for each instance of a document belonging to the snapshot!.documents Array and get back a new Array containing the resultant values of that transformation.
In this case I will use a more specific operation; compactMap. We have to try and cast the returned value from Any to String. If that does not succeed, it will return nil, and we'll want to filter that out. I expect it to be an unlikely case due to the type requirements made by the Firebase Console, but it's good to be aware of it. Here is the example:
func fetchUsernames(from usernameCollection: String, completion: #escaping ([String]) -> Void) {
let db = Firestore.firestore()
let collection = db.collection(usernameCollection)
collection.getDocuments { snapshot, error in
guard error != nil,
let usernames = snapshot?.documents.compactMap { $0.get("username") as? String }
else { return print("Error obtaining usernames") }
completion(usernames)
}
}
The key line here being let usernames = snapshot?.documents.compactMap { $0.get("username") }. We are passing the map function a closure. This closure is passed an argument itself; each value from the snapshot?.documents array. You may refer to this passed in value with $0.

Swift + Firebase. Accessing current user's document

My current firebase structure is Collection of Users which then have a subcollection of habits. For a given user, I want them to be able to add to their own collection of routines. however, running into an issue. When I run the function below, it just creates a separate user with a separate routine. How would I tie a new routine to a current authenticated user?
func addData(routineMsg: String){
let db = Firestore.firestore()
let user = db.collection("users").document()
let routine = db.collection("users").document("GzsHAHq1P0uXGdlYwF8P").collection("routines").document()
routine.setData(["id": routine.documentID, "routine": routineMsg]) { err in
if err != nil {
print((err?.localizedDescription)!)
return
}
}
}
Right now, the code shows how I hard-code it to a certain document (GzsHAHq1P0uXGdlYwF8P), but would like to be able to determine the document dynamically by user
let user = db.collection("users").document()
By not passing document() an argument, what you are doing is creating a new document reference with an auto-generated document ID. What you want to do is pass the method with a string that locates the user's document. Ideally, this would be the user's ID:
guard let uid = Auth.auth().currentUser?.uid else {
return
}
let userDocRef = db.collection("users").document(uid)
From there, to generate random document IDs in the subcollection, do what you were doing before:
func addData(routineMsg: String) {
guard let uid = Auth.auth().currentUser?.uid else {
return
}
let db = Firestore.firestore()
let userDocRef = db.collection("users").document(uid)
let routineDocRef = userDocRef.collection("routines").document()
routineDocRef.setData([
"id": routineDocRef.documentID,
"routine": routineMsg
]) { error in
if let error = error {
print(error)
}
}
}

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.