SwiftUI:在列表中切换按钮状态

SwiftUI: toggle buttons state inside List

我尝试使用曲目列表实现一个简单的播放器应用程序,其中每一行都有一个播放按钮,如果我为一首曲目按下播放,当前正在播放的曲目(如果有)应该停止播放(在本例中为播放按钮应该改变图标)。在这里我有一些问题 - 如何识别当前正在播放的曲目(播放按钮处于播放模式的位置),如何将其重置为初始状态以便一次只播放一首曲目。

在这个例子中,多个按钮可以切换到播放状态,而不是只需要一个按钮

import SwiftUI

class Audio: ObservableObject, Identifiable {
    let id = UUID()
    var title: String
    @Published var isPlaying = false {
        didSet {
            print(isPlaying)
        }
    }
    
    init(title: String) {
        self.title = title
    }
}

class AudiosFetcher: ObservableObject {
    
    @Published var audios = [Audio]()
    
    func fetchAudios() {
        audios = [
            Audio(title: "track 1"),
            Audio(title: "track 2"),
            Audio(title: "track 3")
        ]
    }
    
}

struct ListRow: View {
    
    @ObservedObject var audio: Audio
    
    var body: some View {
        HStack {
            Button(action: {
                audio.isPlaying.toggle()
            }) {
                Image(systemName: audio.isPlaying ? "pause.circle" : "play.circle")
            }
            .buttonStyle(BorderlessButtonStyle())
            .font(.largeTitle)
            
            Text(audio.title)
        }
    }
}

struct ContentView: View {
    
    @ObservedObject var audiosFetcher = AudiosFetcher()
    
    var body: some View {
        List(audiosFetcher.audios, id: \.id) { audio in
            ListRow(audio: audio)
        }.onAppear {
            audiosFetcher.fetchAudios()
        }
    }
}

更新:解决方案

感谢@Yrb 的回答,我们可以这样做了。也许 AudiosFetcher 不是保存当前播放音频的最佳位置,但它可以工作并且可以在需要时提取到单独的实体中

struct ContentView: View {
    // The intialization of the ObservableObject should be a @StateObject,
    // not an @ObservedObject.
    @StateObject var audiosFetcher = AudiosFetcher()
    
    var body: some View {
        List($audiosFetcher.audios) { audio in
            ListRow(audiosFetcher: audiosFetcher, audio: audio)
        }
        .onAppear {
            audiosFetcher.fetchAudios()
        }
    }
}

struct ListRow: View {
    
    @StateObject var audiosFetcher: AudiosFetcher
    @Binding var audio: Audio
    
    var body: some View {
        HStack {
            Button(action: {
                audiosFetcher.playingAudio = (audiosFetcher.playingAudio == audio ? nil : audio)
            }) {
                Image(systemName: audiosFetcher.playingAudio == audio ? "pause.circle" : "play.circle")
            }
            .buttonStyle(BorderlessButtonStyle())
            .font(.largeTitle)
            
            Text(audio.title)
        }
    }
}

struct Audio: Identifiable, Equatable {
    let id = UUID()
    var title: String
    
    init(title: String) {
        self.title = title
    }
}

class AudiosFetcher: ObservableObject {
    
    @Published var audios = [Audio]()
    @Published var playingAudio: Audio?
    
    func fetchAudios() {
        audios = [
            Audio(title: "track 1"),
            Audio(title: "track 2"),
            Audio(title: "track 3")
        ]
    }
}

您可以尝试像这样重置所有音频的初始状态,并且一次只播放一首曲目:

class Audio: ObservableObject, Identifiable {
    let id = UUID()
    var title: String
    @Published var isPlaying = false {
        didSet {
            print(title + " isPlaying: \(isPlaying)")
        }
    }
    
    init(title: String) {
        self.title = title
    }
}

class AudiosFetcher: ObservableObject {
    @Published var audios = [Audio]()
    
    func fetchAudios() {
        audios = [
            Audio(title: "track 1"),
            Audio(title: "track 2"),
            Audio(title: "track 3")
        ]
    }
    
    // -- here
    func toggleAudio(_ audio: Audio) {
       let inState = audio.isPlaying
       audios.forEach{ [=10=].isPlaying = false} // <-- turn all off
       audio.isPlaying = !inState  // <-- only toggle this one
    }
}

struct ContentView: View {
    @StateObject var audiosFetcher = AudiosFetcher() // <-- here
    
    var body: some View {
        List(audiosFetcher.audios, id: \.id) { audio in
            ListRow(audio: audio)
        }.onAppear {
            audiosFetcher.fetchAudios()
        }
        .environmentObject(audiosFetcher) // <-- here
    }
}

struct ListRow: View {
    @EnvironmentObject var audiosFetcher: AudiosFetcher // <-- here
    @ObservedObject var audio: Audio
    
    var body: some View {
        HStack {
            Button(action: {
                audiosFetcher.toggleAudio(audio) // <-- here
            }) {
                Image(systemName: audio.isPlaying ? "pause.circle" : "play.circle")
            }
            .buttonStyle(BorderlessButtonStyle())
            .font(.largeTitle)
            Text(audio.title)
        }
    }
}
 

我修改了一些东西。首先,因为你只想要一个音频,最多,播放我把播放状态从数据模型中拉出来 Audio。我还把它变成了一个结构体,因为这是 SwiftUI 的首选。我将播放状态移到了视图模型中。为了简化代码,我实际上删除了 ListRow,尽管您可以将视图模型注入 ListRow。这为您提供了播放音频的真实来源。如果没有播放音频,它将是 nil。控件已关闭此值:

struct ContentView: View {
    // The intialization of the ObservableObject should be a @StateObject,
    // not an @ObservedObject.
    @StateObject var audiosFetcher = AudiosFetcher()
    
    var body: some View {
        List($audiosFetcher.audios) { $audio in
            HStack {
                Button(action: {
                    audiosFetcher.isPlaying = (audiosFetcher.isPlaying == audio ? nil : audio)
                }) {
                    Image(systemName: audiosFetcher.isPlaying == audio ? "pause.circle" : "play.circle")
                }
                .buttonStyle(BorderlessButtonStyle())
                .font(.largeTitle)
                
                Text(audio.title)
            }
        }
        .onAppear {
            audiosFetcher.fetchAudios()
        }
    }
}

struct Audio: Identifiable, Equatable {
    let id = UUID()
    var title: String
    
    init(title: String) {
        self.title = title
    }
}

class AudiosFetcher: ObservableObject {
    
    @Published var audios = [Audio]()
    @Published var isPlaying: Audio?
    
    func fetchAudios() {
        audios = [
            Audio(title: "track 1"),
            Audio(title: "track 2"),
            Audio(title: "track 3")
        ]
    }
}