You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 01:45:08