Swift: Wait for firestore load before next load - swift

I have a view controller that lists data from a firestore database. Inside a firestore collection, I have a bunch of documents with the information shown in the list, and one document called order which contains one field which is an array of strings in the order I want them displayed. My code grabs this:
self.db.collection("officers").document(school).collection(grade).document("order").getDocument {(document, error) in
if let document = document, document.exists {
self.officerNames = (document.data()!["order"] as! Array<String>)
and then is supposed to use the strings in the array order (officerNames) to query the documents in that same collection (all the documents have a different role so it's only getting one document in the snapshot) and display them in the same order as the one set in order (officerNames).
for item in 1...self.officerNames.count {
self.db.collection("officers").document(school).collection(grade).whereField("role", isEqualTo: self.officerNames[item-1]).getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let officerMessage = document.data()["agenda"] as! String
let officerInfo = document.data()["short"] as! String
(a bunch of code here using that ^ ^ and due to the color I need item to be an integer)
}
}
}
}
I know that if I try printing item before the self.collection("officers")..... the numbers count by one but if I do that in the for document in querySnapshot..... they're all out of order meaning some documents are loaded faster than others. I have read about Async functions in Swift (although I do use those in JavaScript) but am really confused how to use them and hopefully, there is a simpler way to do this. Any way I can wait to make sure the previous document has been loaded and analyzed before iterating through the loop again?
Here's a screenshot of the database:

Honestly, you may want to examine your data structure and see if you can create one that doesn't require multiple queries like this. I can't quite tell what your data structure is, but if you update your question to include it, I can give some suggestions for how to refactor so you don't have to do 2 different get requests.
That being said, since Swift doesn't have promises like JS, it can be tough to keep data in order. For most cases, closures work well, as I wrote about in this blog. But they still won't preserve order in an array of async calls. Assuming you're using some array to store the officer's data, you can declare the size of the array up front by giving each one a default value. This would look something like this:
var officerArray = [Officer](repeating:Officer(), count: self.officerNames.count)
Of course, it'll be different depending on what kind of objects you're populating it with. I'm using some generic Officer object in this case.
Then, rather than appending the newly created Officer object (or whatever you're calling it) to the end of the array, add its value to its particular location in the array.
for item in 1...self.officerNames.count {
self.db.collection("officers").document(school).collection(grade).whereField("role", isEqualTo: self.officerNames[item-1]).getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let officerMessage = document.data()["agenda"] as! String
let officerInfo = document.data()["short"] as! String
// etc
officerArray[self.officerNames.count-1] = Officer(officerMessage: officerMessage, officerInfo: officerInfo) // or however you're instantiating your objects
}
}
}
}
This preserves the order.

Related

Firestore Swift: Get number of nested maps stored in a document

I have a document with fields structured something like this:
Is there any way to count the number of nested maps? So in this case, I want to show that I have two "elements",
JGBQBnFX23Mh5NL4W8f3N1E5Czu1 and JGBQBnFX23Mh5NL4W8f3N1E5Czu1
Right now, I only know how to retrieve everything under "attendees" using
db.collection("Collection")
.document(eventID)
.getDocument { doc, err in
if let err = err {
print(err.localizedDescription)
return
}
let data = doc?.data()
let attendees = data?["attendees"]
print(attendees)
}
But I do not know how to get the total count (of my document IDs)
Keep in mind I will not be able to use arrays in my scenario. Thanks!
It turns out it could be fixed by changing
let attendees = data?["attendees"]
to
let attendees = data?["attendees"] as? Dictionary<String, Any>
and then you could get the total count by doing attendees?.count

Snapshot listener, get the document name, Firebase Swift

I am adding a snapshot listener to a firebase Collection/Document/Collection to get the data as it is updated
The issue I am having is I run through a for loop and as it gets the i in the for loop I then use that string (collection name), to direct the snapshot listener, when the data comes in it will add to the data already there rather than change the data. because it doesn't touch the code to get the collection name, as far as I know.
What I need to do is to be able to add the data to a dictionary that has [String:Any], so I can have the ["collection name":name, "document name": document id, "info":document data], this is fine on first run but when data is changed it only get the data from the changed listener but I don't know how I can get it to get the collection name so I can remove that document from the dictionary before adding the new data.
func getClockOutData(completion: #escaping (_ finished: Bool) -> ()) {
for i in projectsList {
Firestore.firestore().collection("Projects").document(i).collection("TimeSheets").addSnapshotListener { (snapshot, err) in
if let err = err {
print("Tony the error was \(err.localizedDescription)")
} else {
print("Tony dataC A get is \(i)")
if let projDocs = snapshot?.documents {
print("Tony dataC get is \(i)")
for d in projDocs {
let dataG = d.data()
let dateG = d.documentID
let dataCT = ["project":i, "dateG":dateG, "inf":dataG] as [String : Any]
print("Tony dataC is \(dataCT)")
self.dataC.append(dataCT)
}
completion(true)
}
}
}
}
}
How can I get the project name (i) when the snapshot fires again with the changes?
I want that so I can create a for loop to check the project and add all the data from the project to a dict with all the same project name grouped and then run through that loop when there are changes to remove the project info before it is re appended
When you're retrieving the documents from the subcollection, no data from the parent document is retrieved.
So you will either have to load the parent document to get that information, or get the information from the context where you make this request (as likely you've loaded the parent document before as part of determining projectsList).

Delete a specific value in an array from Firestore Swift

I have this code here and I want to delete certain value inside the array "Answered". Is there a simple way to access the first value in the array? This is right but shows what I want to happen "Answered[0]" <- I want to get the first value in that array and delete it. Thank you in Advance
let uid = Auth.auth().currentUser?.uid
print(self.randomArray)
let wash = db.collection("users").document(uid!)
wash.updateData([
"Answered": FieldValue.arrayUnion([self.randomArray])
])
}
if(self.check.isEmpty != true){
self.whichQuestion = self.check[0]
self.whichQuestionString = String(self.whichQuestion)
db.collection("users").document(uid!).updateData([
"Answered": FieldValue.delete(),
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
print("Document successfully updated")
}
}
If your array contains unique values however, you can remove the item with:
self.whichQuestionString = String(self.whichQuestion)
db.collection("users").document(uid!).updateData([
"regions": FieldValue.arrayRemove([whichQuestionString])
])
If you only know the index of the item, there is no way to remove it without knowing the entire array.
The recipe for this is:
Read the document from Firestore
Modify the array in your application code
Write the entire modified array back to Firestore
Also see (none of which unfortunately are for Swift):
Is there any way to update a specific index from the array in Firestore
Delete data from Firestore dynamically

Firebase Firestore Messing Up Order Of Array

I'm using Firebase's Firestore to store data for my iOS app. In Firestore, I have two collections. One called songs and one called playlists. The songs collection contain many documents and each document has a single song info inside of it. Here's what a songs document looks like.
Then, in my collection playlists it contains some documents which are playlists. Here's an example of a playlists document.
I want to be able to display a playlist, and then get the songTitles from that playlist from my songs collection. So, I've done this:
var testPlaylistTitles = [String]()
db.collection("playlists").whereField("title", isEqualTo: "Test Playlist").getDocuments { (snapshot, error) in
for document in (snapshot?.documents)! {
self.testPlaylistTitles = ((document.data()["songTitles"] as? [String])!)
}
}
This makes the variable testPlaylistTitles = ["Test Song", "Test Song 2", "Test Song 3"]. And that's great. But this is where it goes wrong. I want to get information about each one of these songs from my songs collection. So, I create a for loop and loop through the songs to append to my other arrays of artist, images, and titles. Just for this example, I'll use a variable of returedTitles.
var testPlaylistTitles = [String]()
db.collection("playlists").whereField("title", isEqualTo: "Test Playlist").getDocuments { (snapshot, error) in
for document in (snapshot?.documents)! {
testPlaylistTitles = ((document.data()["songTitles"] as? [String])!)
}
var returedTitles = [String]()
for i in 0...testPlaylistTitles.count-1 {
db.collection("songs").whereField("title", isEqualTo: testPlaylistTitles[i]).getDocuments(completion: { (snapshot, error) in
for document in (snapshot?.documents)! {
returedTitles.append((document.data()["title"] as? String)!)
}
})
}
}
Now, returedTitles is equal to a shuffled version of the original testPlaylistTitles. I thought at first that somehow it was ordering testPlaylistTitles by alphabetical order, but it isn't. Does anyone have any ideas on what is happening and how to fix it! Thanks a lot! This has been stumping me for a while.
I have no idea of how to do this in swift, but I would change the data structure of the songs on the playlist document to a object where the songs are the keys and the order the value, then, in the second loop in your query, you use the keys to return the values and you should be able to sort by the values.
{
description: "test",
songs: {
"Test Song 1": 1,
"Test Song 2": 3,
"Test Song 4": 2
},
title: "Test playlist"
}
What I may also suggest is to use the ID's of the songs in the list, as you are retrieving the data anyway at a later stage.
It looks like you're depending on the order of execution of the queries in the for loop. Bear in mind that getDocuments() is asynchronous, meaning it returns immediately, and the results appears in the callback some time later (there is no guarantee how long it will take). Because it's asynchronous, you're effectively kicking off testPlaylistTitles.count nubmer of queries all happening at the same time, each without regard to the other. As a result, the array you're building is going to accumulate the results in an undefined order. To help visualize this, put a log message in each callback to see what order they're actually being invoked. Include contents of the array each time as well, to see how the array is being built.
If the order of the results matters, you will need to figure out what that order is yourself. If that means performing the queries one after the other in order (which would be an overall performance hit), then you'll have to code it that way.
But the bottom line is that you'll need to think carefully about doing asynchronous work. Please read this blog for more information about why Firebase APIs are asynchronous.
As Doug points out, the issue is that the calls are asynchronous, so they won't necessarily be added to the array in the order you start them in--they are added when the data becomes available. There are a few ways to resolve this, but one suggestion to get you started would be to initialize your arrays such that their size is equal to the size of testPlaylistTitles. Then, when you get the data, you insert it in that specific location in the array instead of appending it to the end of the array.
var testPlaylistTitles = [String]()
db.collection("playlists").whereField("title", isEqualTo: "Test Playlist").getDocuments { (snapshot, error) in
for document in (snapshot?.documents)! {
testPlaylistTitles = ((document.data()["songTitles"] as? [String])!)
}
var returedTitles = [String](repeating: "title", count: testPlaylistTitles.count)
for i in 0...testPlaylistTitles.count-1 {
db.collection("songs").whereField("title", isEqualTo: testPlaylistTitles[i]).getDocuments(completion: { (snapshot, error) in
if (snapshot!.documents.count) > 0 {
let doc = (snapshot?.documents.first)!
returedTitles[i] = ((doc.data()["title"] as? String)!)
}
})
}
}
In this code, I've initialized an array the size of testPlaylistTitles. The value of each index is just "title" to begin with, but is then replaced with the value from Firestore once it is downloaded.

Difference between generate and append when querying and adding data to array?

var objectarray = [PFObject]()
func populateTable() {
query.findObjectsInBackgroundWithBlock { (objects, error) in
self.objectarray.removeAll(keepCapacity: true)
self.searchTableView.reloadData()
if error == nil {
Above is the query I am doing and the below 2 codes are what I can use to use the query to populate a array.
if let objects = objects as [PFObject]! {
self.objectarray = Array(objects.generate())
}
Is there any difference with running this code above to populate my array or running the code below?
for object in objects! {
self.objectarray.append(object)
}
Doing either works to load onto my tableView. Also another question regarding Parse. After doing the above, the user doesn't download PFFiles from the background until I run
getDataInBackgroundWithBlock
right? I want to know if it'd be beneficial to save smaller versions of images onto the server.