'Swift, Firebase, array of map: why are the @DocumentIDs for each Album duplicated in each element of the Song array?

I'm working through Javed Davidson's "Music Player in a Day" YouTube tutorial, but with some notable changes and I'm experiencing something very unexpected; namely, I only see one song for each album.

When I walk through the code, I see that the songs have all been given the same DocumentID, which explains the symptom, but not the cause. (RHPS!)

Here's some code:

Firestore data

struct Album: Identifiable, Codable {
    @DocumentID var id: String?
    var title: String
    var image: String
    var releaseDate: Date
    var songs: [Song]
}

struct Song: Identifiable, Codable {
    @DocumentID var id: String?
    var title: String
    var timeInSeconds: Int
}

struct ContentView: View {
    @EnvironmentObject var mpData: MusicPlayerData
    @State private var currentAlbum: Album?
    
    var body: some View {
        NavigationView {
            ScrollView {
                ScrollView(.horizontal, showsIndicators: false) {
                    HStack {
                        ForEach(mpData.albums) { album in
                            AlbumArtView(album: album)
                                .onTapGesture { self.currentAlbum = album }
                        }
                    }
                }
                LazyVStack(alignment: .leading) {
                    if let album = currentAlbum {
                        ForEach(album.songs) { song in
                            SongCellView(album: album, song: song)
                        }
                    }
                }
            }
            .navigationTitle("Bob Seger")
        }
    }
}

If I change the ForEach(album.songs) to ForEach(album.songs, id:\.title), then it works.

But, of course, I'd prefer to use the id property of the struct.

Can someone tell me what I'm doing wrong and how to fix it?



Solution 1:[1]

I had a quick look at Jared's video and it seems like he is not using Codable, but instead maps the documents manually, so I assume using Codable is one of the substantial changes you mention in your question.

The reason why your songs don't get a unique ID is that @DocumentID only works for documents.

In the screenshot we can see that you use Firestore documents for albums, and a nested array for the songs of an album. This means that the albums' document IDs will be mapped to the id attribute of the Album struct.

As a nested array is not a document, it doesn't have a document ID, so all the song ids will be nil, which is why you see only one song per album.

There are couple of ways around this:

  1. Use the title as the identifying attribute (just like you did)
  2. Store all the songs in a top-level collection songs and add an attribute albumID that refers to the album they belong to. This will allow you to fetch all songs belonging to an album using a query. It also allows you to assign a song to multiple albums (which might or might not be what you want).
  3. Use a sub collection for the songs.

How exactly you model your data heavily depends on your use case(s), and there is no one true way of modeling this. I recommend checking out our video series about Firestore to get a deeper understanding of how to model data for Firestore: Get to know Cloud Firestore, in particular Maps, Arrays and Subcollections, Oh My! | Get to know Cloud Firestore #4 - YouTube and How to Structure Your Data | Get to know Cloud Firestore #5 - YouTube.

To learn more about how to map your Firestore documents to/from Swift, check out this comprehensive blog post I wrote. It also contains code snippets that show how to handle mapping errors: Mapping Firestore Data in Swift - The Comprehensive Guide. There is also a fully working sample app that demonstrates all the different ways to map simple data types, custom data types, colors, arrays, dates, geopoints, etc: peterfriese/Swift-Firestore-Guide: The Comprehensive Guide to using Cloud Firestore in Swift.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Peter Friese