Reputation: 4528
I try to implement a simple player app with a tracks list where each row has a play button, and if I press Play for one track, current playing track (if any) should stop playing (in this case Play button should change icon). Here I got some questions - how do I identify current playing track (where Play button is in playing mode), how can I reset it to initial state so that only one track plays at a time.
In this example multiple buttons can be switched to playing state, not exclusively one as desired
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()
}
}
}
Update: solution
Thanks to @Yrb's answer, we can do it like this. Maybe AudiosFetcher
is not the best place to hold current playing audio, but it works and can be extracted to a separate entity if needed
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")
]
}
}
Upvotes: 0
Views: 449
Reputation: 9675
I reworked a few things. First of all, since you only want one audio, at most, playing I pulled the playing state out of the data model Audio
. I also turned it into a struct, as that is the preferred choice for SwiftUI. I moved the playing state into the view model. In order to simplify the code, I actually removed ListRow
, though you could inject the view model into ListRow
. This gives you your source of truth for which audio is playing. It will be nil
if no audio is playing. The controls are keyed off of this value:
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")
]
}
}
Upvotes: 1
Reputation: 36304
you could try something like this to reset all audios initial state, and just play one track at a time:
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{ $0.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)
}
}
}
Upvotes: 1