I've created an audio player using SwiftUI in Xcode 15 Beta, the code as you can see below. However, my current goal is to have the audio player automatically play the next track once the preceding song has finished playing (without requiring a trigger from user). I've seen some people use AVAudioPlayerDelegate, but it makes me more confused, since I'm using Struct and the AVAudioPlayerDelegate cannot conform to class protocol. Since I'm still learning SwiftUI, which one is the best fit in this case?
import SwiftUI
import AVFoundation
struct Song: Identifiable {
let id = UUID()
let name: String
let composer: String
let audioFilename: String
}
struct AudioPlayerView: View {
@State var audioPlayer: AVAudioPlayer!
@State var progress: CGFloat = 0.0
@State private var playing: Bool = false
@State var duration: Double = 0.0
@State var formattedDuration: String = ""
@State var formattedProgress: String = "00:00"
@State var currentSong : Song?
@State var currentIndex : Int?
let songs: [Song] = [
Song(name: "1", composer: "Composer 1", audioFilename: "1.wav"),
Song(name: "2", composer: "Composer 2", audioFilename: "2.wav"),
Song(name: "3", composer: "Composer 3", audioFilename: "3.wav"),
Song(name: "4", composer: "Composer 4", audioFilename: "4.wav"),
Song(name: "5", composer: "Composer 5", audioFilename: "5.wav"),
]
var body: some View {
HStack {
Text(currentSong?.name ?? "Audio Player")
Text(" - ")
Text(currentSong?.composer ?? "Composer")
}
.fontWeight(.semibold)
.font(.title2)
.multilineTextAlignment(.center)
.minimumScaleFactor(0.75)
// the progress bar
HStack {
Text(formattedProgress)
.font(.caption.monospacedDigit())
// this is a dynamic length progress bar
GeometryReader { gr in
Capsule()
.stroke(Color.black, lineWidth: 2)
.background(
Capsule()
.foregroundColor(.black)
.frame(width: gr.size.width * progress,
height: 8), alignment: .leading)
}
.frame( height: 8)
Text(formattedDuration)
.font(.caption.monospacedDigit())
}
.padding()
.frame(height: 50, alignment: .center)
// the control buttons
HStack(alignment: .center, spacing: 20) {
Spacer()
Button(action: {
if audioPlayer.isPlaying {
playing = false
self.audioPlayer.pause()
} else if !audioPlayer.isPlaying {
playing = true
self.audioPlayer.play()
}
}) {
Image(systemName: playing ?
"pause.circle.fill" : "play.circle.fill")
.font(.title)
.imageScale(.large)
.tint(.black)
}
Button(action: {
shuffle()
}) {
Image(systemName: "shuffle")
.font(.title)
.imageScale(.medium)
.tint(.black)
}
Spacer()
}
.onAppear {
let randomSong = songs.randomElement()!
initialiseAudioPlayer(for: randomSong)
}
}
func initialiseAudioPlayer(for song: Song) {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.minute, .second]
formatter.unitsStyle = .positional
formatter.zeroFormattingBehavior = [ .pad ]
// init audioPlayer
let path = Bundle.main.path(forResource: song.audioFilename, ofType: nil)!
self.audioPlayer = try! AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
self.audioPlayer.play()
currentSong = song
playing = true
//The formattedDuration is the string to display
formattedDuration = formatter.string(from:TimeInterval(self.audioPlayer.duration))!
duration = self.audioPlayer.duration
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
if !audioPlayer.isPlaying {
playing = false
}
progress = CGFloat(audioPlayer.currentTime / audioPlayer.duration)
formattedProgress = formatter.string(from: TimeInterval(self.audioPlayer.currentTime))!
}
}
func shuffle() {
if let currentSong = currentSong {
let songs = songs.filter { $0.id != currentSong.id}
if let randomSong = songs.randomElement() {
audioPlayer.stop()
initialiseAudioPlayer(for: randomSong)
}
}
}
}
#Preview {
AudioPlayerView()
}