Here's a SwiftUI version:
MediaStore
import SwiftUI
import Combine
import MediaPlayer
class MediaStore : ObservableObject {
// MARK: - Properties -
@Published var dataArray : [MPMediaItem] = [] {
didSet {
didChange.send(self)
}
}
@Published var songArray : [MPMediaItem] = [] {
didSet {
didChange.send(self)
}
}
@Published var albumArray : [MPMediaItem] = [] {
didSet {
didChange.send(self)
}
}
var didChange = PassthroughSubject<MediaStore, Never>()
// MARK: - Functions -
func getAllArtists() {
let query : MPMediaQuery = MPMediaQuery.artists()
let allArtists = query.collections
dataArray = []
guard allArtists != nil else {
return
}
// loop
for collection in allArtists! {
let item : MPMediaItem? = collection.representativeItem
if item != nil,
item?.playbackStoreID == "" {
dataArray.append(item!)
}
}
}
func getAllSongs(
artistId : NSNumber?
){
let everything : MPMediaQuery = MPMediaQuery.init()
everything.groupingType = MPMediaGrouping.albumArtist
// remove Cloud Items
let predicate = MPMediaPropertyPredicate.init(
value: artistId,
forProperty: MPMediaItemPropertyArtistPersistentID
)
everything.addFilterPredicate(
predicate
)
// songArray
songArray = everything.items ?? []
// albumArray
for song in songArray {
if albumArray.contains(
where: { mediaItem in
mediaItem.albumPersistentID == song.albumPersistentID
}
) {
// do nothing
} else {
albumArray.append(song)
}
}
}
}
AudioPlayerViewModel
import SwiftUI
import AVFoundation
import MediaPlayer
class AudioPlayerViewModel : ObservableObject {
// MARK: - Properties -
var audioPlayer : AVAudioPlayer?
@Published var isPlaying = false
@Published var currentItem : MPMediaItem?
// MARK: - Init -
init() {
}
func setSound(
url: URL
) {
print("setSound: url: \(url)")
do {
self.audioPlayer = try AVAudioPlayer(
contentsOf: url
)
} catch {
print("AVAudioPlayer could not be instantiated.")
}
}
func playOrPause() {
guard let player = audioPlayer else {
return
}
if player.isPlaying {
player.pause()
isPlaying = false
} else {
player.play()
isPlaying = true
}
}
}
ContentView
import SwiftUI
import MediaPlayer
struct ContentView : View {
// MARK: - Properties -
@StateObject var mediaStore : MediaStore = MediaStore()
@StateObject var audioPlayerViewModel = AudioPlayerViewModel()
// MARK: - View -
var body : some View {
// MARK: Player
HStack {
Spacer()
VStack {
HStack {
Spacer()
// title
Text(
audioPlayerViewModel.currentItem?.title ?? "No Song Has Been Selected"
)
Spacer()
}
HStack{
Spacer()
// Rewind
Button {
} label: {
Image(
systemName: "backward.fill"
)
.font(
.title
)
}
// Play / Pause
Button {
audioPlayerViewModel.playOrPause()
} label: {
if audioPlayerViewModel.isPlaying {
Image(
systemName: "pause.fill"
)
.font(
.title
)
} else {
Image(
systemName: "play.fill"
)
.font(
.title
)
}
}
// FastForward
Button {
} label: {
Image(
systemName: "forward.fill"
)
.font(
.title
)
}
Spacer()
}
}
Spacer()
}.frame(
height: 100
)
.padding(
.top,
0.0
)
.background(
.clear
)
.onChange(
of: audioPlayerViewModel.currentItem
) { oldValue, newValue in
audioPlayerViewModel.setSound(
url: audioPlayerViewModel.currentItem!.assetURL!
)
audioPlayerViewModel.playOrPause()
}
// MARK: NavigationView
NavigationView {
ScrollView {
LazyVStack (
alignment: .leading,
spacing: 8.0
) {
ForEach(
mediaStore.dataArray,
id: \.self
) { mediaItem in
// push view
let artistId = mediaItem.value(
forProperty: MPMediaItemPropertyArtistPersistentID
) as! NSNumber
NavigationLink(
destination: SongView(
currentItem: $audioPlayerViewModel.currentItem,
artistId: artistId
)
) {
HStack {
// image
let img : UIImage = mediaItem.artwork?.image(
at: CGSize(
width: 50,
height: 50
)
) ?? UIImage.init()
Image.init(
uiImage: img
)
.resizable()
.scaledToFit()
.frame(
width: 50,
height: 50,
alignment: .leading
)
.padding(
EdgeInsets(
top: 0,
leading: 0.0,
bottom: 0,
trailing: 16.0
)
)
// artist / album
VStack(
alignment: .leading
) {
Text.init(
mediaItem.albumArtist ?? ""
).bold()
}
// space
Spacer()
}.padding(
EdgeInsets(
top: 0,
leading: 16.0,
bottom: 0,
trailing: 16.0
)
)
}
.font(
.system(
size: 16,
weight: .light
)
)
.foregroundColor(
.white
)
// divider
Divider()
}
}
}
}
.navigationBarTitleDisplayMode(
.inline
)
.navigationTitle(
""
)
.navigationBarHidden(
true
)
.onAppear(
perform: {
fetch()
}
)
}
// MARK: - Media -
private func fetch() {
mediaStore.getAllArtists()
}
}
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
SongView
import SwiftUI
import MediaPlayer
import AVKit
struct SongView : View {
// MARK: - Properties -
@StateObject var mediaStore : MediaStore = MediaStore()
@Binding var currentItem : MPMediaItem?
var artistId : NSNumber?
// MARK: - Lifecycle -
var body : some View {
ScrollView {
ForEach(
mediaStore.albumArray,
id: \.self
) { albumItem in
// MARK: Title
HStack {
// Artwork
let img : UIImage = albumItem.artwork?.image(
at: CGSize(
width: 200,
height: 200
)
) ?? UIImage.init()
Image.init(
uiImage: img
)
.resizable()
.scaledToFit()
.frame(
width: 200,
height: 200,
alignment: .leading
)
.padding(
EdgeInsets(
top: 0,
leading: 16.0,
bottom: 0,
trailing: 16.0
)
)
// Title
Text(
albumItem.albumTitle ?? "Unknown"
)
.font(
.system(
size: 36.0
)
)
Spacer()
// Release Date
if #available(macCatalyst 15.0, *) {
Text(
albumItem.releaseDate?.formatted() ?? ""
)
.font(
.system(
size: 16.0
)
)
.padding(
EdgeInsets(
top: 0,
leading: 16.0,
bottom: 0,
trailing: 16.0
)
)
} else {
// Fallback on earlier versions
}
}
// MARK: Album Count
Text(
"\(albumItem.discCount) ALBUM, \(albumItem.albumTrackCount) SONGS"
)
.font(
.system(
size: 16.0
)
)
.foregroundColor(
.gray
)
Divider()
// MARK: Songs
LazyVStack (
alignment: .leading,
spacing: 8.0
) {
ForEach(
mediaStore.songArray,
id: \.self
) { mediaItem in
if mediaItem.albumPersistentID == albumItem.albumPersistentID {
HStack {
// MARK: Thumbnail
// image
let img : UIImage = mediaItem.artwork?.image(
at: CGSize(
width: 50,
height: 50
)
) ?? UIImage.init()
Image.init(
uiImage: img
)
.resizable()
.scaledToFit()
.frame(
width: 50,
height: 50,
alignment: .leading
)
.padding(
EdgeInsets(
top: 0,
leading: 0.0,
bottom: 0,
trailing: 16.0
)
)
// MARK: Play Button
Button {
if mediaItem.isCloudItem == false {
print("mediaItem.assetURL: \(String(describing: mediaItem.assetURL))")
// access security
let gotAccess = mediaItem.assetURL!.startAccessingSecurityScopedResource()
if gotAccess {
print("got access!")
// create player
currentItem = mediaItem
} else {
// warn
}
} else {
print("mediaItem.playbackStoreID: \(mediaItem.playbackStoreID)")
print("mediaItem.assetURL: \(String(describing: mediaItem.assetURL))")
}
} label: {
if mediaItem.isCloudItem {
Image(
systemName: "cloud"
)
.font(
.title
)
} else {
Image(
systemName: "play.circle"
)
.font(
.title
)
}
}
.padding(
EdgeInsets(
top: 0,
leading: 0.0,
bottom: 0,
trailing: 16.0
)
)
// MARK: Track Number
Text.init(
"\(mediaItem.albumTrackNumber)"
)
.foregroundColor(
.white
)
.padding(
EdgeInsets(
top: 0,
leading: 0.0,
bottom: 0,
trailing: 16.0
)
)
// MARK: Artist / Album
// artist / album
VStack(
alignment: .leading
) {
Text.init(
mediaItem.albumArtist ?? ""
).bold()
Text.init(
mediaItem.albumTitle ?? ""
)
}
// space
Spacer()
// title
Text.init(
mediaItem.title ?? ""
)
}.padding(
EdgeInsets(
top: 0,
leading: 16.0,
bottom: 0,
trailing: 16.0
)
)
.opacity(
mediaItem.isCloudItem ? 0.7 : 1.0
)
.gesture(
TapGesture().onEnded(
{ value in
print("tap")
}
)
)
// divider
Divider()
}
}
}
}
}.onAppear(
perform: {
fetch()
}
)
}
// MARK: - Media -
private func fetch() {
mediaStore.getAllSongs(
artistId: artistId
)
}
}
MPMusicPlayerController.systemMusicPlayer
. – Anode