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

SwiftUI如何管理List内ForEach的播放按钮状态 实现单音频播放

List内ForEach生成的播放按钮互斥控制方案

问题需求

在嵌套ForEach的List中存在多个逻辑相似的播放按钮,点击播放按钮可播放CoreData中存储的音频文件,需要解决点击新播放按钮时无法停止当前播放音频的问题,实现同一时间仅一个播放逻辑生效,其余按钮自动切换为未激活状态。

原有实现的问题

原有代码把播放状态playButton和播放器实例audioPlayer定义在单个Cell的局部状态中,每个Cell的状态互相独立,无法感知其他Cell的播放情况,自然无法实现播放互斥。
原有实现代码:

@State var playButton = false // 定义在body外部
Button(action: {
    playButton.toggle()
    do {
        if playButton {
            self.audioPlayer = try AVAudioPlayer(data: songCellVM.song.songMp3!)
            self.audioPlayer?.prepareToPlay()
            self.audioPlayer?.play()       
        } else {
            self.audioPlayer?.stop() 
        }
    } catch let error {
        print("\(error.localizedDescription)")
    }
}){   
    Image(systemName: playButton ? "playpause" : "play")
        .resizable()
        .frame(width: 25, height: 25, alignment: .center)
        .padding()
}

最终实现方案

核心思路是将全局唯一的播放器实例和当前选中播放的歌曲状态抽到上层列表的ViewModel中,所有Cell通过Binding共享这两个全局状态,通过对比当前Cell对应歌曲ID和全局选中歌曲ID判断按钮状态。
完整实现代码:

import Foundation
import SwiftUI
import Combine
import AVFoundation

struct ArtistViewMain: View {
    @ObservedObject var songsListVM = SongListVM()
    @ObservedObject var repo = SongRepository()
    
    var body: some View {
        VStack {
            NavigationView {
                List {
                    ForEach(songsListVM.songCellVMs) { songCellVM in
                        SongCell(songCellVM: songCellVM, selectedSong: $songsListVM.selectedSong, audioPlayer: $songsListVM.audioPlayer)
                    }
                }
                .navigationTitle("Artists")
            }
        }
    }
}

struct SongCell: View {
    @ObservedObject var songCellVM: SongCellVM
    @Binding var selectedSong: Song?
    @Binding var audioPlayer: AVAudioPlayer?
    // 判断当前Cell对应的歌曲是否为正在播放的歌曲
    var isSelected: Bool { songCellVM.song.id == selectedSong?.id }
    var onCommit: () -> () = {  }

    var body: some View {
        HStack {
            Text(songCellVM.song.artistname)
            Spacer()
            Image(uiImage: UIImage(data: songCellVM.song.artistImage ?? Data()) ?? UIImage())
                .resizable()
                .frame(width: 70, height: 70, alignment: .center)
                .scaledToFit()
                .cornerRadius(20)
            
            Button(action: {
                // 先停止当前正在播放的音频
                self.audioPlayer?.stop()
                if isSelected  {
                    // 点击正在播放的按钮,暂停播放
                    selectedSong = nil
                } else {
                    // 点击其他按钮,切换到对应歌曲播放
                    selectedSong = songCellVM.song
                    do {
                        self.audioPlayer = try AVAudioPlayer(data: songCellVM.song.songMp3!)
                        self.audioPlayer?.prepareToPlay()
                        self.audioPlayer?.play()
                    } catch let error {
                        print("\(error.localizedDescription)")
                    }
                }
            }){     
                Image(systemName: isSelected ? "playpause" : "play")
                    .resizable()
                    .frame(width: 25, height: 25, alignment: .center)
                    .padding()
            }
            .buttonStyle(PlainButtonStyle())
            .onDisappear{
                // 页面消失时停止播放,避免后台继续播放
                self.audioPlayer?.stop()
            }
        }
    }
}

内容的提问来源于stack exchange,提问作者multiverse

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 16:15:03