This content originally appeared on Level Up Coding – Medium and was authored by Itsuki

Need to get some music/music video/podcast/audiobook/whatever from user’s library?
Don’t need or want to interact with Apple Music API?
Media Player Framework is here for us!
Let’s build a Simplified Music App (Music Library only) Copy Cat to display some albums and playlists from user’s media library and play some songs!
We will also be listening to any changes performed on the library so that we can update our app accordingly !
Feel free to Grab the full code from my GitHub and let’s start!
Framework Overview
Media Player framework, part of MusicKit framework, or better said, a simplified wrapper, mainly aiming at interacting with user’s Music Library.
Different from MusicKit, we don’t have to add the MusicKit App Service, and of course, we won’t have access to Apple Music.
Here are some of the main UIs/capabilities provided by this framework.
- Play content from the user’s library with one of the built-in MPMusicPlayerController
- Have user selecting media items with MPMediaPickerController
For example, if all we need is to PLAY music locally within your app, (no more, not less), we can use the applicationQueuePlayer.
If all we want to have access to are those media items picked by the user (similar to the PhotoPicker where we will only get the photos selected), we can use the MPMediaPickerController like following.
import SwiftUI
import MediaPlayer
struct ContentView: View {
@State private var showPicker = true
@State private var pickedItemCollection: MPMediaItemCollection? = nil
var body: some View {
NavigationStack {
List {
if let pickedItemCollection {
ForEach(pickedItemCollection.items, id:\.persistentID) { item in
VStack(alignment: .leading) {
Text(item.title ?? "(Unknown)")
Text("Added: \(item.dateAdded)")
}
}
} else {
Text("Nothing picked yet!")
}
}
.sheet(isPresented: $showPicker, content: {
MusicPlayerControllerRepresentable(showPicker: $showPicker, pickedItemCollection: $pickedItemCollection)
})
.toolbar(content: {
ToolbarItem(placement: .topBarTrailing, content: {
Button(action: {
showPicker = true
}, label: {
Image(systemName: "plus")
})
})
})
}
}
}
struct MusicPlayerControllerRepresentable: UIViewControllerRepresentable {
@Binding var showPicker: Bool
@Binding var pickedItemCollection: MPMediaItemCollection?
var mediaTypes: MPMediaType = .music
var allowsPickingMultipleItems: Bool = true
var showsCloudItems: Bool = true
var prompt: String? = nil
var showsItemsWithProtectedAssets: Bool = true
typealias UIViewControllerType = UINavigationController
func makeUIViewController(context: Context) -> UIViewControllerType {
let navigationController = UINavigationController()
let controller = MPMediaPickerController(mediaTypes: mediaTypes)
controller.allowsPickingMultipleItems = allowsPickingMultipleItems
controller.showsCloudItems = showsCloudItems
controller.prompt = prompt
controller.showsItemsWithProtectedAssets = showsItemsWithProtectedAssets
controller.delegate = context.coordinator
navigationController.present(controller, animated: true)
return navigationController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
func makeCoordinator() -> Coordinator {
return .init(self)
}
class Coordinator: NSObject, MPMediaPickerControllerDelegate {
var parent: MusicPlayerControllerRepresentable
init(_ parent: MusicPlayerControllerRepresentable) {
self.parent = parent
}
// A method that the system calls when a user selects a set of media items.
func mediaPicker(_ mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection) {
print(#function)
print("did pick media items: \(mediaItemCollection)")
self.parent.pickedItemCollection = mediaItemCollection
self.parent.showPicker = false
}
// A method that the system calls when a user taps Cancel to dismiss a media item picker.
func mediaPickerDidCancel(_ mediaPicker: MPMediaPickerController) {
print(#function)
mediaPicker.dismiss(animated: false)
self.parent.showPicker = false
}
}
}

(The behavior of this picker is actually really sketchy, a lot more than it looks above, when used with SwiftUI, that’s why I have shared the code here for you to give it a try yourself!)
Using both of the controllers above requires the same NSAppleMusicUsageDescription key as we will set up later, but will NOT require us to ask to handle permission related stuff within our code.
Now!
Back to what we are interested in this article!
This Media Player framework also allows us to interact with user’s media library directly, without using those built-in UIs (I don’t want to have UIViewControllerRepresentable here and there in my app)!
We can use it to get/search for audio content, such as songs, podcasts, and books, in the user’s library, get playlists, add items to playlist and etc!
We can then do what ever we want to those media items, for example, playing it using an AVAudioPlayer by ourselves!
Set up
Thankfully, as I have mentioned above, we don’t have to deal with the MusicKit App Service!
So!
All we have to do is to add this NSAppleMusicUsageDescription to our info.plist.
Note this framework will only work correctly on real devices so make sure to also select a development team under Signing and Capabilities!
Request/Check access
Since we are interacting with the media library directly, we will need to check for authorization status and ask for permission if not granted yet.
To get the current status, we have the type method authorizationStatus() on MPMediaLibrary , and to request for authorization, we can use requestAuthorization()
var status: MPMediaLibraryAuthorizationStatus = MPMediaLibrary.authorizationStatus()
func requestAuthorization() async {
self.status = await MPMediaLibrary.requestAuthorization()
}
Basic Operations
Let’s first take a look at how we can get those media items, some of the things we can configure, couple little points to keep in mind.
We will then put everything together for a simple music library copy cat!
Get Everything
The MPMediaQuery class is our main interaction point with user’s library.
Let’s start with the most basic one where we will get all media items in the library, no grouping, no filtering.
import Playgrounds
import SwiftUI
import MediaPlayer
#Playground(body: {
let everythingQuery = MPMediaQuery()
}
To perform the query and get the items, it is as simple as calling the items property.
This will return an optional [MPMediaItem] where the nil represents a query error.
let items = everythingQuery.items ?? []
The MPMediaItem here contains pretty much everything we are interested in!
For example, we can get album related information such as albumArtist, albumTitle, and albumTrackCount. We can get the artist, the cover image with artwork, the URL we can use to play the music with assetURL!
There are just more and more! Check out the documentation and I bet you will find what you need here!
We will see how we can use those properties to build up our UI! In couple Seconds!
Grouping & Filtering
We can either configure the grouping/filtering by by ourselves, or we can use one of the built-in convenience constructors such as songs() that will return a MPMediaQuery with Filter and Grouping type specified.
And of course, we can combine the two!
For example, let’s add some filters and groupingType to our everythingQuery so that we get the same thing as using the songs() function.
let everythingQuery = MPMediaQuery()
everythingQuery.groupingType = .title
let songFilter = MPMediaPropertyPredicate(
value: 2049, // for either MPMediaType.music.rawValue (1) or MPMediaType.musicVideo.rawValue (2048)
forProperty: MPMediaItemPropertyMediaType,
comparisonType: .equalTo
)
everythingQuery.addFilterPredicate(songFilter)
let songQuery = MPMediaQuery.songs()
assert(everythingQuery.filterPredicates == songQuery.filterPredicates)
assert(everythingQuery.groupingType == songQuery.groupingType)
Here is a little map for what each convenience constructors have for their filter and groupingType.
| constructor | filter | groupingType |
|----------------|-------------------------------------------|------------------------------|
| albums() | music | MPMediaGrouping.album |
| artists() | music | MPMediaGrouping.artist |
| audiobooks() | audioBook | MPMediaGrouping.title |
| compilations() | any with MPMediaItemPropertyIsCompilation | MPMediaGrouping.album |
| composers() | any | MPMediaGrouping.composer |
| genres() | any | MPMediaGrouping.genre |
| playlists() | any | MPMediaGrouping.playlist |
| podcasts() | podcast | MPMediaGrouping.podcastTitle |
| songs() | music | MPMediaGrouping.title |
Note that the composers() , genres() , and playlists() will match the entire library.
Of course, we can further narrow down the result by applying more filters.
For example, in addition to matching only music or musicVideo media type, we also want to filter by the title of the item.
let songTitleFilter = MPMediaPropertyPredicate(
value: "some title",
forProperty: MPMediaItemPropertyTitle,
comparisonType: .contains
)
everythingQuery.addFilterPredicate(songTitleFilter)
For a list of property keys that we can use when creating MPMediaPropertyPredicate , please check out General media item property keys and Media entity property keys!
Note!
When getting metadata for a media item by calling the value(forProperty:)method, we can also use the keys defined in User-defined property keys. However, do NOT use those to build media property predicates!
If we are not sure whether if a specific property key is supported for filtering, we can use the canFilter(byProperty:) type method on MPMediaEntity to check!
print(MPMediaEntity.canFilter(byProperty: MPMediaItemPropertyDateAdded))
// false
Collections
Instead of the items property we had above, we can also use collections to get our query result.
It is an array of media item collections whose contained items match the query’s media property predicate.
For example, if we have used the albums() constructor above, each MPMediaItemCollection will represent an album and we can get the MPMediaItem within each collection using the items property.
Query Section
MPMediaQuerySection defines a range of media items or media item collections from within a media query and the title for it.
We obtain an array of media query sections by using the itemSections or collectionSections properties of a media query. This can be super useful when building user interface so that we can group the items within a section and display some useful headers.
Sounds pretty abstract? We will see how does those come to place when we build our views!
Listen For Library Changes
Of course, user can modify their library at any time outside of our app.
To ask the media library to turn on notifications for whenever the library changes, we can use the beginGeneratingLibraryChangeNotifications() function on the default instance of the MPMediaLibrary, and to end the notifications, we can call endGeneratingLibraryChangeNotifications().
After calling the beginGeneratingLibraryChangeNotifications, the changes will come through the MPMediaLibraryDidChange notification.
extension Notification.Name {
var publisher: NotificationCenter.Publisher {
return NotificationCenter.default.publisher(for: self)
}
}
// ...
self.cancellable = Notification.Name.MPMediaLibraryDidChange.publisher.receive(
on: DispatchQueue.main
).sink { notification in
// refresh any cached data
}
A Little note here about the beginGeneratingLibraryChangeNotifications function.
It is nest-able and it is possible for us to call it multiple times. To turn off notifications, we will have to call endGeneratingLibraryChangeNotifications the same number of times that we called beginGeneratingLibraryChangeNotifications.
Put It Together
Time to create our little copy cat!
MusicManager
First of all, our manager class!
@Observable
@MainActor
class MusicManager: NSObject {
var status: MPMediaLibraryAuthorizationStatus = MPMediaLibrary.authorizationStatus()
var albums: ([MPMediaItemCollection], [MPMediaQuerySection]) = ([], [])
var playlists: [MPMediaPlaylist] = []
var selectedItem: MPMediaItem? {
didSet {
guard oldValue?.assetURL != self.selectedItem?.assetURL else { return }
guard let item = self.selectedItem, let url = item.assetURL else {
self.player?.stop()
self.player = nil
return
}
do {
self.player?.stop()
self.player = try AVAudioPlayer(contentsOf: url)
} catch(let error) {
print("error creating player: \(error)")
self.player = nil
}
}
}
var player: AVAudioPlayer? {
didSet {
if self.player != oldValue {
self.isPlaying = false
}
}
}
// player.isPlaying is not a published variable
var isPlaying: Bool = false {
didSet {
if self.isPlaying {
self.player?.play()
} else {
self.player?.pause()
}
}
}
private let query = MPMediaQuery.songs()
private var cancellable: AnyCancellable?
override init() {
super.init()
self.configureAudioSession()
Task {
self.initializeAlbums()
}
Task {
self.initializePlaylist()
}
// listen for updates
MPMediaLibrary.default().beginGeneratingLibraryChangeNotifications()
self.cancellable = Notification.Name.MPMediaLibraryDidChange.publisher.receive(
on: DispatchQueue.main
).sink { notification in
Task {
self.initializeAlbums()
}
Task {
self.initializePlaylist()
}
}
}
deinit {
MPMediaLibrary.default().endGeneratingLibraryChangeNotifications()
}
private func configureAudioSession() {
do {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.playback, mode: .default, options: [.duckOthers])
try session.setActive(true)
} catch(let error) {
print(error)
}
}
private func initializeAlbums() {
query.groupingType = .album
let collections = query.collections ?? []
let sections: [MPMediaQuerySection] = query.collectionSections ?? []
self.albums = (collections, sections)
}
private func initializePlaylist() {
query.groupingType = .playlist
let collections = query.collections ?? []
let playlist = collections.map { $0 as? MPMediaPlaylist }.filter({$0 != nil}).map({$0!})
self.playlists = playlist
}
func requestAuthorization() async {
self.status = await MPMediaLibrary.requestAuthorization()
}
}
extension MusicManager: AVAudioPlayerDelegate {
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
if player == self.player {
self.isPlaying = false
}
}
}
extension Notification.Name {
var publisher: NotificationCenter.Publisher {
return NotificationCenter.default.publisher(for: self)
}
}
In addition to the interactions with the media library above, I have also added some logic for playing the music using AVAudioPlayer. If you don’t need to play the media item within your app, you can ignore it!
Also, in addition to the albums, I have also decided to add the playlists because it is a little different from other MPMediaItemCollection!
The MPMediaPlaylist inherits from MPMediaItemCollection with some additional functions and properties!
For example, we have the name for the name of the playlist, and the descriptionText that describes the playlist.
In addition, we can also add media items to a playlist using the add(_:) or the addItem(withProductID:) function.
Views
struct ContentView: View {
@State private var musicManager = MusicManager()
var body: some View {
let status = musicManager.status
Group {
switch status {
case .authorized:
LibraryView()
.environment(self.musicManager)
case .denied:
ContentUnavailableView(label: {
Label("Access Denied", systemImage: "xmark.square")
}, description: {
Text("The app doesn't have permission to access media library. Please grant the app access in Settings.")
.multilineTextAlignment(.center)
})
case .notDetermined:
ContentUnavailableView(label: {
Label("Unknown Access", systemImage: "questionmark.app")
}, description: {
Text("The app requires access to the media library.")
.multilineTextAlignment(.center)
}, actions: {
Button(action: {
Task {
await musicManager.requestAuthorization()
}
}, label: {
Text("request access")
})
})
case .restricted:
ContentUnavailableView(label: {
Label("Restricted Access", systemImage: "lock.square")
}, description: {
Text("This device doesn't allow access to media library. Please update the permission in Settings.")
.multilineTextAlignment(.center)
})
@unknown default:
ContentUnavailableView(label: {
Label("Unknown", systemImage: "ellipsis.rectangle")
}, description: {
Text("Unknown authorization status.")
.multilineTextAlignment(.center)
})
}
}
}
}
struct LibraryView: View {
@Environment(MusicManager.self) private var musicManager
@State var audioPlayer: AVAudioPlayer?
var body: some View {
NavigationStack {
List {
NavigationLink(destination: {
AlbumsView()
.environment(self.musicManager)
}, label: {
HStack(spacing: 24) {
Image(systemName: "square.stack")
.foregroundStyle(.pink)
Text("Albums")
}
})
.listRowBackground(Color.clear)
NavigationLink(destination: {
PlaylistsView()
.environment(self.musicManager)
}, label: {
HStack(spacing: 24) {
Image(systemName: "music.note.list")
.foregroundStyle(.pink)
Text("Playlists")
}
})
.listRowBackground(Color.clear)
}
.navigationTitle("Music Library")
.safeAreaInset(edge: .bottom, content: {
PlayerView()
.environment(self.musicManager)
})
}
}
}
struct PlaylistsView: View {
@Environment(MusicManager.self) private var musicManager
var body: some View {
let playlists = musicManager.playlists
List {
if playlists.isEmpty {
Text("No playlists found.")
.foregroundStyle(.gray)
.frame(maxWidth: .infinity, alignment: .leading)
.listRowBackground(Color.clear)
.listRowInsets(.vertical, 8)
.listRowInsets(.horizontal, 0)
}
ForEach(playlists, id: \.self) { playlist in
NavigationLink(destination: {
MediaItemCollectionView(collection: playlist, isPlaylist: true)
.environment(self.musicManager)
}, label: {
HStack {
if let image = playlist.image {
image
.scaledToFit()
.aspectRatio(1, contentMode: .fit)
.frame(width: 64, height: 64)
.clipShape(RoundedRectangle(cornerRadius: 8))
} else {
Image(systemName: "gearshape")
.font(.system(size: 24))
.aspectRatio(1, contentMode: .fit)
.foregroundStyle(.gray.opacity(0.6))
.frame(width: 64, height: 64)
.background(.gray.opacity(0.2))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
Text(playlist.name ?? "(Unknown Playlist)")
.lineLimit(1)
.foregroundStyle(.black)
}
})
}
.listRowBackground(Color.clear)
.listRowInsets(.vertical, 8)
.listRowInsets(.horizontal, 0)
}
.navigationTitle("Playlists")
.navigationBarTitleDisplayMode(.large)
.safeAreaInset(edge: .bottom, content: {
PlayerView()
.environment(self.musicManager)
})
}
}
struct AlbumsView: View {
@Environment(MusicManager.self) private var musicManager
var body: some View {
let (albums, sections): ([MPMediaItemCollection], [MPMediaQuerySection]) = musicManager.albums
ScrollView {
if albums.isEmpty {
Text("No albums found.")
.foregroundStyle(.gray)
.padding(.all, 16)
.frame(maxWidth: .infinity, alignment: .leading)
}
LazyVGrid(columns: .init(repeating: .init(.flexible(minimum: UIScreen.main.bounds.width/3), spacing: 12), count: 2), spacing: 24, content: {
ForEach(sections, id: \.self) { section in
Section {
let albumsInSection: [MPMediaItemCollection] = Array(albums[section.range.lowerBound..<section.range.upperBound])
ForEach(albumsInSection, id: \.self) { album in
NavigationLink(destination: {
MediaItemCollectionView(collection: album, isPlaylist: false)
.environment(self.musicManager)
}, label: {
VStack(alignment: .leading) {
if let image = album.image {
image
.scaledToFit()
.aspectRatio(1, contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 8))
} else {
Image(systemName: "music.note")
.font(.system(size: 48))
.aspectRatio(1, contentMode: .fit)
.foregroundStyle(.gray.opacity(0.6))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.gray.opacity(0.2))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
Text(album.albumTitle)
.font(.subheadline)
.lineLimit(1)
.foregroundStyle(.black)
Text(album.albumArtist)
.lineLimit(1)
.font(.caption)
.foregroundStyle(.gray)
}
})
.navigationLinkIndicatorVisibility(.hidden)
}
} header: {
Text(section.title)
.frame(maxWidth: .infinity, alignment: .leading)
.fontWeight(.bold)
}
.contentMargins(.top, 0)
}
})
.padding(.horizontal, 16)
.padding(.vertical, 24)
}
.navigationTitle("Albums")
.navigationBarTitleDisplayMode(.large)
.safeAreaInset(edge: .bottom, content: {
PlayerView()
.environment(self.musicManager)
})
}
}
struct MediaItemCollectionView: View {
@Environment(MusicManager.self) var musicManager
var collection: MPMediaItemCollection
var isPlaylist: Bool = false
var body: some View {
let playlist = collection as? MPMediaPlaylist
let title = isPlaylist ? playlist?.name ?? "Unknown Playlist" : collection.albumTitle
let subtitle: String? = isPlaylist ? playlist?.descriptionText : collection.albumArtist
let totalCount: Int = collection.count
let totalDuration: TimeInterval = collection.totalDuration
let items: [MPMediaItem] = collection.items
List {
Section {
VStack(spacing: 8) {
Image(systemName: "gearshape")
.font(.system(size: 120))
.foregroundStyle(.gray.opacity(0.6))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.aspectRatio(1, contentMode: .fit)
.background(.gray.opacity(0.2))
.clipShape(RoundedRectangle(cornerRadius: 16))
.padding(.horizontal, 16)
.padding(.vertical, 8)
VStack {
Text(title)
.font(.title3)
.fontWeight(.bold)
if let subtitle {
Text(subtitle)
.font(.default)
.foregroundStyle(.gray)
}
}
Divider()
}
.listRowBackground(Color.clear)
}
.listSectionSpacing(0)
.listSectionMargins(.top, 0)
Section {
if items.isEmpty {
Text("No items in this \(isPlaylist ? "playlist" : "album").")
.font(.default)
.frame(maxWidth: .infinity, alignment: .center)
.foregroundStyle(.gray)
.listRowBackground(Color.clear)
}
ForEach(items, id: \.self) { item in
Button(action: {
self.musicManager.selectedItem = item
}, label: {
HStack {
if let image = item.image {
image
.scaledToFit()
.aspectRatio(1, contentMode: .fit)
.frame(width: 40, height: 40)
.clipShape(RoundedRectangle(cornerRadius: 8))
} else {
Image(systemName: "music.note")
.font(.system(size: 24))
.aspectRatio(1, contentMode: .fit)
.foregroundStyle(.gray.opacity(0.6))
.frame(width: 40, height: 40)
.background(.gray.opacity(0.2))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
VStack(alignment: .leading) {
Text(item.title ?? "(Unknown item)")
.lineLimit(1)
if let artist = item.artist {
Text(artist)
.font(.caption)
.foregroundStyle(.gray)
.lineLimit(1)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
})
.buttonStyle(.plain)
}
.listRowBackground(Color.clear)
.listRowInsets(.vertical, 8)
.listRowInsets(.horizontal, 0)
}
.listSectionSpacing(0)
.listSectionMargins(.top, -8)
if !items.isEmpty {
Section {
VStack(spacing: 16) {
Divider()
Group {
let songs = "\(totalCount) \(totalCount <= 1 ? "song" : "songs")"
if let formattedString = totalDuration.formattedString {
Text("\(songs), \(formattedString)")
} else {
Text("\(songs)")
}
}
.font(.subheadline)
.foregroundStyle(.gray)
.frame(maxWidth: .infinity, alignment: .leading)
}
.listRowBackground(Color.clear)
}
.listSectionSpacing(0)
.listSectionMargins(.top, -8)
}
}
.safeAreaInset(edge: .bottom, content: {
PlayerView()
.environment(self.musicManager)
})
}
}
struct PlayerView: View {
@Environment(MusicManager.self) private var musicManager
var body: some View {
if musicManager.player != nil, let selectedItem = musicManager.selectedItem {
HStack(spacing: 24) {
if let image = selectedItem.image {
image
.scaledToFit()
.aspectRatio(1, contentMode: .fit)
.frame(width: 40, height: 40)
.clipShape(RoundedRectangle(cornerRadius: 8))
.layoutPriority(1)
} else {
Image(systemName: "music.note")
.font(.system(size: 24))
.aspectRatio(1, contentMode: .fit)
.foregroundStyle(.gray.opacity(0.6))
.frame(width: 40, height: 40)
.background(.gray.opacity(0.2))
.clipShape(RoundedRectangle(cornerRadius: 8))
.layoutPriority(1)
}
Text(selectedItem.title ?? "(Unknown)")
.font(.headline)
.layoutPriority(1)
Spacer()
Button(action: {
musicManager.isPlaying.toggle()
}, label: {
Image(systemName: musicManager.isPlaying ? "pause.fill" : "play.fill")
.font(.title)
.frame(width: 40, height: 40)
})
.buttonStyle(.plain)
}
.padding(.vertical, 8)
.padding(.horizontal, 24)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
Capsule()
.fill(.white.opacity(0.8))
)
.padding(.horizontal, 16)
}
}
}
extension MPMediaItemCollection {
var image: Image? {
return self.representativeItem?.image?.resizable()
}
var albumTitle: String {
if let representativeItem = self.representativeItem {
return representativeItem.albumTitle ?? "(Unknown album)"
}
return "(Unknown album)"
}
var albumArtist: String {
if let representativeItem = self.representativeItem {
return representativeItem.albumArtist ?? "(Unknown artist)"
}
return "(Unknown artist)"
}
var totalDuration: TimeInterval {
return self.items.reduce(0) { result, item in
result + item.playbackDuration
}
}
}
extension MPMediaItem {
var image: Image? {
if let artwork = self.artwork, let uiImage = artwork.image(at: artwork.bounds.size) {
return Image(uiImage: uiImage)
.resizable()
}
return nil
}
}
extension TimeInterval{
var formattedString: String? {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .full
formatter.allowedUnits = [.hour, .minute]
return formatter.string(from: self)
}
}
It is pretty long, but I still decided to paste it here so that we can get an idea of how those MPMediaQuerySections come into play!

Thank you for reading!
That’s it for this article!
Again, feel free to Grab everything from my GitHub!
Happy music-ing!
SwiftUI: Access Music Library with MediaPlayer Framework was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding – Medium and was authored by Itsuki