I'm having the same problem, but with a List with the searchable modifier. Clicking on the cancel button or cross button to remove the text causes this.
Post
Replies
Boosts
Views
Activity
Hi @RanLearns!
I was in the same position as you when Apple released beta 4. I was able to fix the issues mainly around the play() being asynchronous now and can throw an error. I don't know if my approach is the standard, one but I'll share it nonetheless.
Dividing the error into two parts -
Trying to make an async call from a non-async function.
The call can throw errors, but the function is not handling the error.
For the first part, we can suffix the function with the async keyword and prefix the call with the await keyword.
For example, `handlePlayButtonSelected() becomes -
private func handlePlayButtonSelected() async {
if !isPlaying {
if !isPlaybackQueueSet {
player.setQueue(with: album)
isPlaybackQueueSet = true
}
await player.play()
} else {
player.pause()
}
}
We can make the function throw an error and prefix the call with the try keyword for the second part. So, handlePlayButtonSelected() finally looks like -
private func handlePlayButtonSelected() async throws {
if !isPlaying {
if !isPlaybackQueueSet {
player.setQueue(with: album)
isPlaybackQueueSet = true
}
try await player.play()
} else {
player.pause()
}
}
Similarly, `handlePlayButtonSelected() can be changed to -
private func handleTrackSelected(_ track: Track, loadedTracks: MusicItemCollection<Track>) async throws {
player.setQueue(with: loadedTracks, startingAt: track)
isPlaybackQueueSet = true
try await player.play()
}
Now, you'll get another error in view where you call these functions. First, in the list where you show the TrackCell, update the action handleTrackSelected(track, loaded tracks: loadedTracks) with the following -
Task {
try? await handleTrackSelected(track, loadedTracks: loadedTracks)
}
For the second error in the button in playButtonRow, update handlePlayButtonSelected with -
Task {
try? await handlePlayButtonSelected()
}
The third error is due to a change in the searchable modifier. Now, you've to specify a prompt.
So, replace searchable("Albums", text: $searchTerm) with -
.searchable(text: $searchTerm, prompt: "Albums")
There are a few warnings as well, mostly related to the use of detach. You can replace them with Task.detached.
Lastly, you'll find deprecation warnings related to the setQueue(with:startingAt:) method. In Xcode 13, Beta 4 added a new instance property, queue.
We can set this property by using the initializers of the class Queue. In AlbumDetailView.handleTrackSelected(_:loadedTracks:), you can set it as -
private func handleTrackSelected(_ track: Track, loadedTracks: MusicItemCollection<Track>) async throws {
player.queue = .init(for: loadedTracks, startingAt: track)
isPlaybackQueueSet = true
try await player.play()
}
In handlePlayButtonSelected(), you can set it as -
private func handlePlayButtonSelected() async throws {
if !isPlaying {
if !isPlaybackQueueSet {
player.queue = .init(arrayLiteral: album)
isPlaybackQueueSet = true
}
try await player.play()
} else {
player.pause()
}
}
The last warning is related to the deprecation of the playbackStatus variable. The latest beta offers us with ObservableObject class MusicPlayer.State, and we'll use the playbackState instance property instead. I'm not sure about this one on how to go about observing the value of the playbackStatus, but here's my approach -
.task {
isPlaying = player.state.playbackStatus == .playing
}
With this, the project is error and warnings-free!
Hi @JoeKun,
Thank you for your detailed answer! Much appreciated.
So, if I've to show the current duration in a label, I should probably have a timer running with the initial value of playbackTime? And then, whenever the playbackStatus changes, update the current duration value with the latest playbackTime and refresh the view?
I think I can do with this approach for the time being. But having playbackTime as an observable value will make it much much simpler. So, I'll file a ticket for this.
Thank you so much!
Hi @joekun,
This is the custom struct -
struct Songs: Decodable {
let data: [Song]
}
where Song is the new structure in MusicKit for Swift.
Hi @JoeKun,
While creating my own framework for easy access to different Apple Music APIs, I realized I was using it wrong. It's better to decode using MusicItemCollection<> instead of creating my own structure. Then, it safely ignores the item that is not decodable due to missing information.
let response = try JSONDecoder().decode(MusicItemCollection<Song>.self, from: dataResponse.data)
What should be the best approach to decode the chart data for songs?
For example -
struct Charts: Decodable {
let results: Songs
}
struct Songs: Codable {
let songs: [ChartsSong]
}
struct ChartsSong: Codable {
let data: [Song]
let orderID, next, chart, name: String
let href: String
enum CodingKeys: String, CodingKey {
case data
case orderID = "orderId"
case next, chart, name, href
}
}
let url = URL(string: "https://api.music.apple.com/v1/catalog/us/charts?types=songs&genre=20&limit=1")!
let musicRequest = MusicDataRequest(urlRequest: URLRequest(url: url))
let response = try await musicRequest.response()
let data = try JSONDecoder().decode(Charts.self, from: response.data)
Right now, I'm using a custom structure to decode the data with the ChartsSong struct. Do you think there's a possibility of a Chart item in the future? Should I file feedback stating my use case?
Hi @JoeKun,
Apologies for the delay. I've filed feedback - FB9647701
Thanks!
Edit: I didn't see that this question was for MusicKit JS. Apologies.
I don't know how to delete the answer, so I'll leave it for someone else to find it helpful.
You can extract the song ID from the link by creating a URL component and getting the query items out of the component.
Here, the query parameter is "i" with the value 1035048414 as the song ID. You get the first query item and fetch the value out of it:
let components = URLComponents(string: "https://music.apple.com/us/album/take-on-me-1985-12-mix-2015-remastered/1035047659?i=1035048414")
guard let songID = components?.queryItems?.first?.value else { return }
print("SONG ID IS - \(songID)")
Now, you can just use a standard MusicCatalogResourceRequest:
let request = MusicCatalogResourceRequest<Song>(matching: \.id, equalTo: MusicItemID(songID))
do {
let response = try await request.response()
print(response.description)
} catch {
print(error)
}
When I print it:
SONG ID IS - 1035048414
MusicCatalogResourceResponse<Song>(
items: [
Song(id: "1035048414", title: "Take On Me (1985 12" Mix) [2015 Remastered]", artistName: "a-ha")
]
)
I hope that helps!
I faced the same problem and wrote an article as a workaround!
https://rudrank.blog/musickit-for-swift-artist-artwork
Try wrapping the view inside ForEach with a VStack:
List {
ForEach(intervals, id: \.id) { interval in
Section {
VStack { // <-- HERE
Text(interval.name)
}
.id(interval.id)
}
}
}
Were you folks able to figure out a solution or a workaround to this? Is it on Apple's side?
Oh yes, if you see my code example, Genres is a typealias for MusicItemCollection<Genre>! :D
MusicItemCollection is one of the best structures in MusicKit, and it simplifies so many things.
Wow, this is interesting. Really want to know how Apple deals with this
You can get the "Favourite Mix" and "New Music Mix" if you hit the recommendations API that returns a collection of playlists. Have you tried that?
https://api.music.apple.com/v1/me/recommendations
Hi @JoeKun, with the current solution and the response from the endpoint, it only fetches one item at a time. How would I go about creating a MusicItemCollection out of it? The Search for Catalog Resources has a very nice response as it provides an array of albums, songs, etc.
But the Get Catalog Search Suggestions has one response model per music item, be it an album, song. Etc.
Any suggestion?
Edit: Hacky workaround
switch topResult.content {
case .album(let album): self.albums += MusicItemCollection(arrayLiteral: album)
case .artist(let artist): self.artists += MusicItemCollection(arrayLiteral: artist)
case .song(let song): self.songs += MusicItemCollection(arrayLiteral: song)
case .curator(let curator): self.curators += MusicItemCollection(arrayLiteral: curator)
default: ()
}
Do you think I should use MusicItemCollection in this way?