swift for loop order of data is not right - swift

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

Related

Array populated from Firebase returned empty, but data was retrieved. Why?

Still learning some swift and managed to advance and retrieving data from a firestore database. I have a Data Controller whose task is to offload all the data retrieving from firestore. It does the calls and gets data, but when returning the info from the first function I have implemented on it, it's empty.
Here's an example of the funcion:
func fetchUnidades(for foo: MyFirstEnum, and bar: MySecondEnum ) -> [MyClassType]{
let db = Firestore.firestore()
let colPath = "my/firebase/path"
let results = [MyClassType]()
let collection = db.collection(colPath)
collection.whereField("myField", isEqualTo: foo.rawValue).getDocuments() { querySnapshot, err in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
do {
print("\(document.documentID) => \(document.data())") // 1st print
let newMyClass = try document.data(as: MyClassType.self)
results.append(newMyClass)
print("here") // 2nd print - debug breakpoint
}catch (let error) {
print("\(error)")
}
}
}
}
print("DC - Recovered \(results.count) results")
return results
}
Assume MyFirstEnum, MySecondEnum and MyClassType are correct, because the database retrieves info. On the 1st print line, there's output for data retrieved, and on the 2nd print - debug breakpoint line, if I do a po results, it has one value, which is the one retrieved as you can see here:
unidades being the name on my code of results on this example.
But right after continuing with the execution, unidades, aka results is empty, the line:
print("DC - Recovered \(results.count) results")
prints DC - Recovered 0 results and the return also returns an empty array with zero values on it.
Any idea about why this might be happening? And how to solve the issue? Obviously the goal is to return the info...
That's because the result comes asynchronously. Your fetchUnidades returns results array before it's populated.
You need to add a completion closure in this case. Instead of returning results you call that completion closure and pass the results as its argument.
func fetchUnidades(for foo: MyFirstEnum, and bar: MySecondEnum, completion: (results: [MyClassType]?, error: Error?) -> Void) {
let db = Firestore.firestore()
let colPath = "my/firebase/path"
let collection = db.collection(colPath)
collection.whereField("myField", isEqualTo: foo.rawValue).getDocuments() { querySnapshot, err in
if let err = err {
print("Error getting documents: \(err)")
completion(nil, err)
} else {
let results = [MyClassType]()
for document in querySnapshot!.documents {
do {
print("\(document.documentID) => \(document.data())") // 1st print
let newMyClass = try document.data(as: MyClassType.self)
results.append(newMyClass)
print("here") // 2nd print - debug breakpoint
}catch (let error) {
print("\(error)")
}
}
completion(results, nil)
}
}

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

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

Query to count number followers swift 4

I'm trying to count the number of followers the user has from my firebase database but no success. The number of followers is returning any value. When a user follows another user it creates a node in firebase with the userID and a Int value of 1 as shown below.
fileprivate func countTrusting() {
guard let userId = self.user?.uid else { return }
guard let currentLoggedInUser = Auth.auth().currentUser?.uid else { return }
Database.database().reference().child("following").child(userId).queryOrderedByValue().queryEqual(toValue: 1).observe(DataEventType.value, with: { (snapshot) in
let count = String(snapshot.childrenCount)
self.trustedLabel.text = "\(count) \nFollowing"
}) { (error) in
print("There was an error getting the following count:", error)
}
}
My firebase database
If I understand the question, you want to have a count of the number of followers of a user. In that case, there's no need for a query as the only nodes that will exist are the users followers.
This code reads in the child followers nodes and prints the count.
let userFollowingRef = fbRootRef.child("following").child("some_user_uid")
userFollowingRef.observeSingleEvent(of: .value, with: { snapshot in
print(snapshot.childrenCount)
})

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.