我在 Xcode 15 Beta 中使用 SwiftUI 创建了一个音频播放器,代码如下所示。但是,我当前的目标是让音频播放器在前一首歌曲播放完毕后自动播放下一首曲目(不需要用户触发)。我见过有些人使用 AVAudioPlayerDelegate,但这让我更加困惑,因为我使用的是 Struct 并且 AVAudioPlayerDelegate 不能符合类协议。由于我仍在学习 SwiftUI,哪一个最适合这种情况?
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()
}
ObservableObject
和 AVAudioPlayerDelegate
的类来管理音频播放并在歌曲结束时通知您的视图:import AVFoundation
import Combine
class AudioPlayerManager: NSObject, ObservableObject, AVAudioPlayerDelegate {
static let shared = AudioPlayerManager()
private var audioPlayer: AVAudioPlayer?
private var cancellables: Set<AnyCancellable> = []
@Published var currentSong: Song?
private override init() {
super.init()
}
func play(song: Song) {
guard let path = Bundle.main.path(forResource: song.audioFilename, ofType: nil) else {
return
}
do {
audioPlayer?.stop()
audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
audioPlayer?.delegate = self
audioPlayer?.play()
currentSong = song
} catch {
print("Error playing audio: \(error)")
}
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
if flag, let currentSong = currentSong,
let index = AudioPlayerView.songs.firstIndex(where: { $0.id == currentSong.id }),
index < AudioPlayerView.songs.count - 1 {
play(song: AudioPlayerView.songs[index + 1])
}
}
}
AudioPlayerView
以使用 AudioPlayerManager
及其方法来控制音频播放:struct AudioPlayerView: View {
// ... Your existing properties
@ObservedObject private var audioPlayerManager = AudioPlayerManager.shared
// ... Your existing code
var body: some View {
// ... Your existing UI
HStack(alignment: .center, spacing: 20) {
Spacer()
Button(action: {
if audioPlayerManager.currentSong != nil {
audioPlayerManager.play(song: audioPlayerManager.currentSong!)
} else {
let randomSong = songs.randomElement()!
audioPlayerManager.play(song: randomSong)
}
}) {
Image(systemName: audioPlayerManager.isPlaying ? "pause.circle.fill" : "play.circle.fill")
.font(.title)
.imageScale(.large)
.tint(.black)
}
// ... Your existing shuffle button and Spacer
}
.onAppear {
let randomSong = songs.randomElement()!
audioPlayerManager.play(song: randomSong)
}
}
// ... Your existing functions
static 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"),
]
}
确保根据您的特定需求调整此代码并对其进行彻底测试,以确保其按预期工作。