Firebase Firestore Messing Up Order Of Array - swift

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.

Related

Using Cell table, sum of all values ​in firestore

I have a firestore file, all files add the value of the field "quantity" (all together and displayed in the label)
For example, the value of "Quantity" in the first file field is "1", the value of "Quantity" in the second file field is "2", and the value of "Quantity" in the third file field is "2.3"" 1+2+2.3= Displayed in the label: 5.3 as shown in Figure 2
But Form 2 automatically changes X2 to 10.6
Form three becomes 15.9
Only 5.3 in Table 1 is correct, why?
Here is my code
db.collection("Admin").document("AI智能機器人").collection("Ai日獲利")
//.order(by: "date", descending: true)
//.whereField("date", isGreaterThanOrEqualTo: dateStr)
//.whereField("date", isLessThanOrEqualTo: date3)
.getDocuments() { (snapshot, error) in
if snapshot!.documents.isEmpty {
}else{
if snapshot?.isEmpty != true && snapshot != nil {
for document in snapshot!.documents{
if let quantity = document.data()["quantity"] as? Double{
self.totalVotes += quantity
cell.nowq8.text = "+"+String(format: "%.4f", self.totalVotes)+"%"
print("quantity:\(self.totalVotes)")[![enter image description here][3]][3]
}
}
}
}
"quantity" sliding tableview will continue to accumulate, what should I do? It has troubled me for a long time.. A little help is greatly appreciated!
Firestore listeners by design are made such that you will always be delivered the document(s) relevant to the fetch or query and the updates as long as the lisenter remain active,which results in multiple read and call fo the function that results in adding up of field value with every refresh and update call.There is no mode to receive deltas only.
The first query snapshot contains added events for all existing documents that match the query. This is because you're getting a set of changes that bring your query snapshot current with the initial state of the query.So each time you refresh your page you are calling again the onSnapshot() method "from scratch" and therefore you get the logic added to in every next run.
Updated
db.collection("Admin").document("AI智能機器人").collection("Ai日獲利")
.getDocuments() { (snapshot, error) in
if let snapshot = snapshot {
if snapshot.documents.isEmpty {
// Handle the case where there are no documents
} else {
for document in snapshot.documents {
if(document?.data())
let quantity = document.data()["quantity"] as? Double {
self.totalVotes += quantity
cell.nowq8.text = "+"+String(format: "%.4f", self.totalVotes)+"%"
}
}
}
} else {
// Handle the error
}
}
Also do checkout these links for similar implementations:
Firestore document is read multiple times
Updating same document twice
Add data to field once even after page refresh

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

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.

Swift: How do I repeat a getDoc request to Firestore?

I can already check if a doc exists in a collection. However I am unable to repeatedly check the same collection while trying different path names.
For example, my collection name is the UID of the user. There can be an unlimited amount of docs in this collection. The docs are titled "UID-0", "UID-1", "UID-2" and so on as the user adds items.
Every time it finds a doc that already exists such as "UID-0" it will change the path request to "UID-+=1" until the number exceeds the docs and it is able to create and use that path name.
Each doc contains about a dozen fields of the same data model but of course different data.
var docAlreadyExists: Bool = true
var multipleUserFencesIdCount: Int = 0
var newID: String = ""
let id = self.auth.currentUser?.uid ?? ""
repeat {
print("1")
self.fencesInfoCollection.document("Live").collection(id).document(newID).getDocument(completion: { document, error in
print("2")
if let document = document, document.exists {
print("EXISTS")
multipleUserFencesIdCount += 1
newID = newID.dropLast() + "\(multipleUserFencesIdCount)"
} else {
print("DOES NOT EXIST")
docAlreadyExists = false
}
})
} while docAlreadyExists
With that said, how can I repeatedly check if a document exists until the path name exceeds the rest and is able to create a new doc with the new data.
Edit:
The 1 gets repeatedly called correctly but the .getDoc never calls since 2 is never printed.
I figured out a better solution to my goal, instead of trying to repeat a call with different IDs I am now getting all documents and counting how many are in the collection.
self.fencesInfoCollection.document(id).collection("Live").getDocuments(completion: { document, error in
if let document = document, document.isEmpty {
print("EMPTY")
} else {
print("DOC1: \(String(describing: document?.count))")
}
})

Swift: Wait for firestore load before next load

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.