如何让SwiftUI音频播放器在曲目结束后自动播放下一曲?
SwiftUI音频播放器实现自动播放下一曲的方案
你需要实现当前曲目播放结束后自动切换下一曲,由于SwiftUI的View是Struct无法遵循AVAudioPlayerDelegate协议,最稳妥且符合SwiftUI设计模式的方案是把音频播放核心逻辑抽离到ObservableObject类(即ViewModel)中,利用类可遵循协议的特性实现代理监听。
具体实现步骤
1. 拆分音频播放逻辑到ViewModel
新建一个遵循ObservableObject和AVAudioPlayerDelegate的类,将原View中的音频初始化、状态管理、切歌逻辑全部迁移至此。这样既可以通过代理监听播放结束事件,又能通过@Published属性将状态同步给View。
2. 实现播放结束回调
在ViewModel中实现audioPlayerDidFinishPlaying(_:successfully:)方法,当当前曲目播放完成时,自动切换到下一首(这里沿用你原有的随机切歌逻辑)。
3. 简化View层代码
原View仅负责UI展示,通过@ObservedObject绑定ViewModel,响应状态变化自动更新UI。
修改后的完整代码
import SwiftUI import AVFoundation // 歌曲模型保持不变 struct Song: Identifiable { let id = UUID() let name: String let composer: String let audioFilename: String } // 音频播放ViewModel,负责核心逻辑处理 class AudioPlayerViewModel: ObservableObject, AVAudioPlayerDelegate { @Published var audioPlayer: AVAudioPlayer? @Published var progress: CGFloat = 0.0 @Published var playing: Bool = false @Published var formattedDuration: String = "" @Published var formattedProgress: String = "00:00" @Published var currentSong: Song? 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"), ] // 复用时间格式化器,避免重复创建 private let timeFormatter: DateComponentsFormatter = { let formatter = DateComponentsFormatter() formatter.allowedUnits = [.minute, .second] formatter.unitsStyle = .positional formatter.zeroFormattingBehavior = [.pad] return formatter }() private var progressTimer: Timer? init() { // 初始化时随机选择一首歌曲播放 if let randomSong = songs.randomElement() { initializeAudioPlayer(for: randomSong) } } func initializeAudioPlayer(for song: Song) { // 清理之前的定时器和播放器 progressTimer?.invalidate() audioPlayer?.stop() // 初始化新播放器 guard let filePath = Bundle.main.path(forResource: song.audioFilename, ofType: nil) else { print("未找到音频文件:\(song.audioFilename)") return } let audioURL = URL(fileURLWithPath: filePath) do { audioPlayer = try AVAudioPlayer(contentsOf: audioURL) audioPlayer?.delegate = self // 设置代理监听播放状态 audioPlayer?.play() currentSong = song playing = true // 更新时长显示 formattedDuration = timeFormatter.string(from: TimeInterval(audioPlayer?.duration ?? 0)) ?? "00:00" // 启动进度更新定时器 progressTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in guard let self = self, let player = self.audioPlayer else { return } self.progress = CGFloat(player.currentTime / player.duration) self.formattedProgress = self.timeFormatter.string(from: TimeInterval(player.currentTime)) ?? "00:00" self.playing = player.isPlaying } } catch { print("初始化播放器失败:\(error.localizedDescription)") } } // 播放结束代理方法,自动切换下一曲 func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { guard flag else { return } if let currentSong = currentSong { // 过滤掉当前歌曲,从剩余歌曲中随机选一首 let availableSongs = songs.filter { $0.id != currentSong.id } if let nextSong = availableSongs.randomElement() { initializeAudioPlayer(for: nextSong) } else { // 只剩当前歌曲时重新播放 initializeAudioPlayer(for: currentSong) } } } // 播放/暂停切换 func togglePlayPause() { guard let player = audioPlayer else { return } player.isPlaying ? player.pause() : player.play() playing = player.isPlaying } // 手动随机切歌 func shuffle() { guard let currentSong = currentSong else { return } let availableSongs = songs.filter { $0.id != currentSong.id } if let randomSong = availableSongs.randomElement() { initializeAudioPlayer(for: randomSong) } } // 销毁时清理定时器,避免内存泄漏 deinit { progressTimer?.invalidate() } } // UI视图,仅负责渲染和用户交互 struct AudioPlayerView: View { @ObservedObject var viewModel = AudioPlayerViewModel() var body: some View { VStack(spacing: 20) { // 歌曲信息展示 HStack { Text(viewModel.currentSong?.name ?? "Audio Player") Text(" - ") Text(viewModel.currentSong?.composer ?? "Composer") } .fontWeight(.semibold) .font(.title2) .multilineTextAlignment(.center) .minimumScaleFactor(0.75) // 进度条区域 HStack { Text(viewModel.formattedProgress) .font(.caption.monospacedDigit()) GeometryReader { geo in Capsule() .stroke(Color.black, lineWidth: 2) .background( Capsule() .foregroundColor(.black) .frame(width: geo.size.width * viewModel.progress, height: 8), alignment: .leading ) } .frame(height: 8) Text(viewModel.formattedDuration) .font(.caption.monospacedDigit()) } .padding() .frame(height: 50) // 控制按钮区域 HStack(alignment: .center, spacing: 20) { Spacer() Button(action: viewModel.togglePlayPause) { Image(systemName: viewModel.playing ? "pause.circle.fill" : "play.circle.fill") .font(.title) .imageScale(.large) .tint(.black) } Button(action: viewModel.shuffle) { Image(systemName: "shuffle") .font(.title) .imageScale(.medium) .tint(.black) } Spacer() } } .padding() } } #Preview { AudioPlayerView() }
关键说明
- 职责分离:ViewModel负责音频播放逻辑,View专注UI渲染,符合SwiftUI的MVVM架构,代码更易维护。
- 可靠监听:使用系统提供的
AVAudioPlayerDelegate回调监听播放结束,比Timer轮询状态更精准、高效。 - 自动状态同步:ViewModel中的
@Published属性会自动通知View更新UI,无需手动处理状态绑定。
内容的提问来源于stack exchange,提问作者Amelia Citra
相关产品推荐
相关产品推荐

