Jacob Cavin
Jacob Cavin

Reputation: 2319

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.

songs documet

Then, in my collection playlists it contains some documents which are playlists. Here's an example of a playlists document.

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.

Upvotes: 1

Views: 2670

Answers (3)

Jen Person
Jen Person

Reputation: 7546

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.

Upvotes: 0

Doug Stevenson
Doug Stevenson

Reputation: 317467

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.

Upvotes: 0

Stephan Grobler
Stephan Grobler

Reputation: 469

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.

Upvotes: 2

Related Questions