Having trouble getting MusicKit Data into my SwiftUI view

Having trouble getting the data from MusicKit Searches back to my SwiftUI view. Normally I would use a @State variable for the decodable structure, but with MusicKit you dont need to decode anything and I can't figure out how to best get the data back to my view. I call the MusicKit function using .onAppear in my SwiftUI view

func getAlbum() async throws -> MusicItemCollection<Album> {

        var request = MusicCatalogResourceRequest<Album>(matching: \.id, equalTo: "1440881121")

        let response = try await request.response()

        print(response.debugDescription)

        return response.items       

printing debugDescription give these results:

MusicCatalogResourceResponse<Album>(

  items: [

    Album(

      id: "1440881121",

      title: "Colors",

      artistName: "Beck",

      contentRating: "explicit",

      copyright: Fonograf Records/Capitol Records; ℗ 2017 Fonograf Records, under exclusive license to UMG Recordings, Inc.,

      genreNames: [

        "Alternative",

        "Music"

      ],

      isCompilation: false,

      isComplete: true,

      isDigitalMaster: true,

      isSingle: false,

      releaseDate: "2017-10-13",

      trackCount: 11,

      upc: "00602557176827"

Hoe would you access all of the data in the debugDescription and use it in your SwiftUI view? I can only access the standard name and id. Is anyone using Codable to move the data through a data model? The documentation is not making sense to me even though I am usually ok using it.

Also, should I be using `func getAlbum() async throws -> MusicCatalogResourceResponse {' instead of using MusicItemCollection?
Thanks

Accepted Reply

Hello @AnimalOnDrums,

There are a couple of things that can be greatly simplified in your code.

First of all, for the List, you don't have to pass the album as the data, since the items of the list are static anyway.

Second, you don't have to store the tracks separately.

Lastly, I would urge you to be a lot more careful with your usage of the force-unwrap operator, as it can easily cause your app to crash if you forget to check that an optional contains a non-nil value.

To that end, let me offer an alternate implementation of your view that uses MusicKit for Swift as it was intended.

Let's start with a convenience extension on Optional to display values that might be missing:

extension Optional {
    var displayableOptionalValue: String {
        let displayableOptionalValue: String
        if let value = self {
            displayableOptionalValue = "\(value)"
        } else {
            displayableOptionalValue = "not available"
        }
        return displayableOptionalValue
    }
}

Then, let's factor out the code for the items of your list into a reusable view, to reduce the amount of duplicated code:

struct MyMusicAttributeView<V>: View {
    let label: String
    let value: V?
    
    var body: some View {
        Text("\(label): \(value.displayableOptionalValue)")
            .foregroundColor(.secondary)
            .padding(2)
    }
}

With this, you can implement your ContentView more simply like this:

struct MyView: View {
    @State private var album: Album?
    @State private var selection: Set<Track.ID> = []
    @State private var sortOrder = [
        KeyPathComparator<Track>(\.trackNumber, order: .forward)
    ]
    
    var body: some View {
        content
            .frame(minWidth: 1000, minHeight: 800)
            .task {
                let authorizationStatus = await MusicAuthorization.request()
                if authorizationStatus == .authorized {
                    var request = MusicCatalogResourceRequest<Album>(matching: \.id, equalTo: "1440881121")
                    request.properties = [.tracks]
                    let response = try? await request.response()
                    self.album = response?.items.first
                }
            }
    }
    
    @ViewBuilder
    private var content: some View {
        if let album = self.album {
            details(for: album)
        } else {
            Text("Loading…")
        }
    }
    
    private func details(for album: Album) -> some View {
        VStack(alignment: .leading) {
            HStack {
                if let artwork = album.artwork {
                    ArtworkImage(artwork, width: 400)
                }
                
                List {
                    MyMusicAttributeView(label: "album id", value: album.id)
                    MyMusicAttributeView(label: "title", value: album.title)
                    MyMusicAttributeView(label: "artist", value: album.artistName)
                    MyMusicAttributeView(label: "composer", value: album.artistName)
                    MyMusicAttributeView(label: "total tracks", value: album.trackCount)
                    MyMusicAttributeView(label: "genres", value: album.genreNames.joined(separator: ", "))
                    MyMusicAttributeView(label: "release date", value: album.releaseDate?.formatted(date: .abbreviated, time: .omitted))
                    MyMusicAttributeView(label: "record label", value: album.recordLabelName)
                    MyMusicAttributeView(label: "copyright", value: album.copyright)
                    MyMusicAttributeView(label: "upc", value: album.upc)
                }
            }
            
            if let standardEditorialNotes = album.editorialNotes?.standard {
                MyMusicAttributeView(label: "editorialNotes.standard", value: standardEditorialNotes)
            }
            
            if let tracks = album.tracks {
                Table(tracks, selection: $selection, sortOrder: $sortOrder) {
                    TableColumn("track", value: \.trackNumber.displayableOptionalValue)
                    TableColumn("title", value: \.title)
                    TableColumn("artist", value: \.artistName)
                    TableColumn("release date") { track in
                        Text((track.releaseDate?.formatted(date: .abbreviated, time: .omitted)).displayableOptionalValue)
                    }
                    TableColumn("duration", value: \.duration.displayableOptionalValue)
                    TableColumn("isrc", value: \.isrc.displayableOptionalValue)
                }
            }
        }
    }
}

As shown here, you rarely need to type out MusicItemCollection in your app-level code. There is often a simpler and more elegant solution, such as just storing the top level music item you're trying to display, such as the Album, in this case.

I hope this helps.

Best regards,

Replies

Hello @AnimalOnDrums,

MusicKit is designed to be used directly with SwiftUI. You can store pretty much any object from MusicKit's model layer as an @State property, including MusicCatalogResourceResponse, MusicItemCollection, or more simply Album.

Here's an example of how you can use an Album to power a SwiftUI View:

struct MyView: View {
    @State var album: Album?
    
    var body: some View {
        Group {
            if let album = self.album {
                VStack {
                    if let artwork = album.artwork {
                        ArtworkImage(artwork, width: 300)
                    }
                    Text(album.title)
                    Text(album.artistName)
                }
            } else {
                Text("Loading…")
            }
        }
        .task {
            let request = MusicCatalogResourceRequest<Album>(matching: \.id, equalTo: "1440881121")
            let response = try? await request.response()
            self.album = response?.items.first
        }
    }
}

For more examples, I highly encourage you to check out our official sample app and our introductory WWDC session: Meet MusicKit for Swift.

I hope this helps.

Best regards,

Hey @JoeKun, Thanks so much for responding! I was able some code similar to what you posted, but I used MusicItemCollection() instead. I find it is easier for me to use it as a variable instead of a Type but I am not sure if this is correct. My code is shown below. What is the difference between using "@State var album: Album?" and "@State var album: MusicItemCollection?"

So this question is only part of my overall issue which is getting the results into a SwiftUI Table. When trying to use a Table in your code (based off of the Garden official sample app I get errors such as: "Album.Type cannot conform to RandomAccessCollection" or "Initializer 'init(_:columns:)' requires that 'Album' conform to 'RandomAccessCollection'". I am sure it can be done, I just don't know what steps I am missing. In my code I can make a Table but I have to convert each value into a string at the TableColumn. So what is the best way to do this? How would you make a Table (like the one I have in my code) of all of the album details, or a table of the album's tracks in the code you provided?

Thanks so much!

struct ContentView: View {

    @State private var albumDetails = MusicItemCollection<Album>()

    @State private var albumTracks = MusicItemCollection<Track>()

    @State private var selection = Set<Track.ID>()

    @State var sortOrder: [KeyPathComparator<Track>] = [

        .init(\.trackNumber, order: SortOrder.forward)

    ]

    

    

    var body: some View {

        VStack {

            HStack {

                if let artwork = albumDetails.first?.artwork {

                    ArtworkImage(artwork, width: 400, height: 400)

                    

                }



            List(albumDetails, id:\.id) { album in

                Text("album id: \(album.id.rawValue)")

                    .foregroundColor(.secondary)

                    .padding(2)

                Text("title: \(album.title)")

                    .foregroundColor(.secondary)

                    .padding(2)

                Text("artist: \(album.artistName)")

                    .foregroundColor(.secondary)

                    .padding(2)

                Text("composer: \(album.artistName)")

                    .foregroundColor(.secondary)

                    .padding(2)

                Text("total tracks: \(album.trackCount)")

                    .foregroundColor(.secondary)

                    .padding(2)

                Text("genres: \(album.genreNames.joined(separator: ", "))")

                    .foregroundColor(.secondary)

                    .padding(2)

                Text("release date: \((album.releaseDate?.formatted(date: .abbreviated, time: .omitted))!)")

                    .foregroundColor(.secondary)

                    .padding(2)

                Text("record label: \(album.recordLabelName ?? "not available")")

                    .foregroundColor(.secondary)

                    .padding(2)

                Text("copyright: \(album.copyright ?? "not available")")

                    .foregroundColor(.secondary)

                    .padding(2)

                Text("upc: \(album.upc ?? "not available")")

                    .foregroundColor(.secondary)

                    .padding(2)

            }

            .font(.system(size: 14))

            .onAppear {

                albumAPI().requestMusicAuthorization()

                async {

                    self.albumDetails = try await albumAPI().getAlbum()

                    let firstAlbum = albumDetails.first

                    self.albumTracks = (firstAlbum?.tracks)!

                    print(firstAlbum!)

                    }

                }

            }

            

            if (albumDetails.first?.editorialNotes?.standard) != nil {

            Text("\(albumDetails.first?.editorialNotes?.standard ?? "")")

                .font(.system(size: 14))

                .foregroundColor(.secondary)

                .padding(2)

                }

        

            

            Table(albumTracks, selection: $selection, sortOrder: $sortOrder) {

                TableColumn("track") { track in

                    Text(String(track.trackNumber!))

                }

                TableColumn("title", value: \.title)

                TableColumn("artist", value: \.artistName)

                TableColumn("release date") { track in

                    Text((track.releaseDate?.formatted(date: .abbreviated, time: .omitted))!)

                }

                TableColumn("duration") { track in

                    Text(String(track.duration!))

                }

                TableColumn("isrc") { track in

                    Text(String(track.isrc!))

                }

            }

        }

    }

}
    ```

Hello @AnimalOnDrums,

There are a couple of things that can be greatly simplified in your code.

First of all, for the List, you don't have to pass the album as the data, since the items of the list are static anyway.

Second, you don't have to store the tracks separately.

Lastly, I would urge you to be a lot more careful with your usage of the force-unwrap operator, as it can easily cause your app to crash if you forget to check that an optional contains a non-nil value.

To that end, let me offer an alternate implementation of your view that uses MusicKit for Swift as it was intended.

Let's start with a convenience extension on Optional to display values that might be missing:

extension Optional {
    var displayableOptionalValue: String {
        let displayableOptionalValue: String
        if let value = self {
            displayableOptionalValue = "\(value)"
        } else {
            displayableOptionalValue = "not available"
        }
        return displayableOptionalValue
    }
}

Then, let's factor out the code for the items of your list into a reusable view, to reduce the amount of duplicated code:

struct MyMusicAttributeView<V>: View {
    let label: String
    let value: V?
    
    var body: some View {
        Text("\(label): \(value.displayableOptionalValue)")
            .foregroundColor(.secondary)
            .padding(2)
    }
}

With this, you can implement your ContentView more simply like this:

struct MyView: View {
    @State private var album: Album?
    @State private var selection: Set<Track.ID> = []
    @State private var sortOrder = [
        KeyPathComparator<Track>(\.trackNumber, order: .forward)
    ]
    
    var body: some View {
        content
            .frame(minWidth: 1000, minHeight: 800)
            .task {
                let authorizationStatus = await MusicAuthorization.request()
                if authorizationStatus == .authorized {
                    var request = MusicCatalogResourceRequest<Album>(matching: \.id, equalTo: "1440881121")
                    request.properties = [.tracks]
                    let response = try? await request.response()
                    self.album = response?.items.first
                }
            }
    }
    
    @ViewBuilder
    private var content: some View {
        if let album = self.album {
            details(for: album)
        } else {
            Text("Loading…")
        }
    }
    
    private func details(for album: Album) -> some View {
        VStack(alignment: .leading) {
            HStack {
                if let artwork = album.artwork {
                    ArtworkImage(artwork, width: 400)
                }
                
                List {
                    MyMusicAttributeView(label: "album id", value: album.id)
                    MyMusicAttributeView(label: "title", value: album.title)
                    MyMusicAttributeView(label: "artist", value: album.artistName)
                    MyMusicAttributeView(label: "composer", value: album.artistName)
                    MyMusicAttributeView(label: "total tracks", value: album.trackCount)
                    MyMusicAttributeView(label: "genres", value: album.genreNames.joined(separator: ", "))
                    MyMusicAttributeView(label: "release date", value: album.releaseDate?.formatted(date: .abbreviated, time: .omitted))
                    MyMusicAttributeView(label: "record label", value: album.recordLabelName)
                    MyMusicAttributeView(label: "copyright", value: album.copyright)
                    MyMusicAttributeView(label: "upc", value: album.upc)
                }
            }
            
            if let standardEditorialNotes = album.editorialNotes?.standard {
                MyMusicAttributeView(label: "editorialNotes.standard", value: standardEditorialNotes)
            }
            
            if let tracks = album.tracks {
                Table(tracks, selection: $selection, sortOrder: $sortOrder) {
                    TableColumn("track", value: \.trackNumber.displayableOptionalValue)
                    TableColumn("title", value: \.title)
                    TableColumn("artist", value: \.artistName)
                    TableColumn("release date") { track in
                        Text((track.releaseDate?.formatted(date: .abbreviated, time: .omitted)).displayableOptionalValue)
                    }
                    TableColumn("duration", value: \.duration.displayableOptionalValue)
                    TableColumn("isrc", value: \.isrc.displayableOptionalValue)
                }
            }
        }
    }
}

As shown here, you rarely need to type out MusicItemCollection in your app-level code. There is often a simpler and more elegant solution, such as just storing the top level music item you're trying to display, such as the Album, in this case.

I hope this helps.

Best regards,

@JoeKun,

Wow! Thank you so much for this! It clears up a lot of the confusion I was having with MusicKit. I originally stored the tracks separately since that's how it was done in the official sample app but I see now why it is not needed. The Optional extension is a great idea too and really makes things easier. I am still trying to get my Table to be sortable and then write the contents of the Table to a CSV file and I think the cleaner code and Optional extension will make it easier to figure out. It is amazing how much less code I am writing using SwiftUI with MusicKit versus Apple Music API and AppKit. Thanks again for all of your support!

Hi @JoeKun, So I have seen a couple of questions, including mine(https://developer.apple.com/forums/thread/697719), regarding the use of .onTapGesture on a Table Row. Could you please take a look at that? basically I want to be able to double click a row and have that open a new view. Currently it seems that this will only work on text in the row. We need to be able to double click anywhere in the row, similar to how you can select a row by clicking on it anywhere.

Next is the issue is that, and toolbars will only appear at the bottom of the screen and not the top. I have seen many unanswered help requests regarding this subject and I will post mine as well, but is this intentional? or is this a bug? I even took code directly from the apple developer documentation on toolbars, which shows a toolbar at the top of the screen, but when I ran it in Xcode the toolbar goes to the bottom.

Thanks so much!

Hello @AnimalOnDrums,

I'm sorry, but I'm going to have to defer to folks working on SwiftUI for those questions.

My team is focused on MusicKit, and as much as I try to be as helpful as possible with questions that are partly about SwiftUI when they're tangentially related to MusicKit, I have be reasonable about the amount of time I spend on the developer forums, so I can also work on actually improving MusicKit itself.

Your other thread is tagged appropriately with SwiftUI, so you've done everything right so it would bubble up to the list of threads monitored by other engineers focused on SwiftUI.

I hope you'll understand.

Best regards,

@JoeKun,

no problem, that makes sense and I completely understand! As always, thanks for taking the time to respond and thanks again for all of your help with MusicKit!

p.s. in case you are ever wondering what items could be added to improve MusicKit, it would be great if MusicKit could include song/track BMP, and the Artist Bio information seen in Apple Music, including birth date and where they are from. Lyrics would be a game changer 😉.

Hi again @AnimalOnDrums,

Thanks for the input! We've definitely heard people asking for some of these, especially BPM and lyrics.

However, it always helps us when external developers like yourself file an actual ticket on Feedback Assistant with a succinct summary of their enhancement request; there is always so much to do, and too little time; so we often use explicit signals from external developers, like those tickets, as a way to measure the relative importance of various features, so we can prioritize them in the best possible way. And generally, getting duplicate tickets for the same enhancement request is actually helpful for us; while we'll of course mark them as duplicate, we then have a very clear signal in the form of the duplicate count, which is also used when we prioritize features on our roadmap.

In this case, if you can spare a few minutes, it would be great for you to file three separate tickets on Feedback Assistant; one for BPM, one for Artist Bio, and one for lyrics. They don't have to be long; just a succinct and clear statement about the request and a short summary of the use-case you have in mind for them.

I hope this helps.

Best regards,