Reputation: 9143
I have an audio file (its length is > 60 seconds) that is played in the onAppear()
method of 2 of my views (shown below).
When I play the audio file in GamesList.swift, it plays the whole length. But if I play it in ActiveGame.swift, it cuts off after a couple seconds. I don't know what the difference is between these views, but it does not work for the latter.
GamesList.swift (successfully plays whole file)
struct GamesList: View {
@State var showCreateGameSheet = false
let diameter: CGFloat = 25.0
@State var games: [Game] = []
@EnvironmentObject var gameManager: GameManager
@State var stayPressed = false
@ObservedObject var soundManager = SoundManager()
var body: some View {
NavigationView {
VStack {
// Create game
HStack {
Spacer()
Button(action: { showCreateGameSheet.toggle() }){
Text("+ Create game")
.font(.custom("Seravek-Medium", size: 20))
.foregroundColor(Color.white)
}.buttonStyle(GTButton(color: Color("primary"), stayPressed: stayPressed))
}
.frame(
maxWidth: .infinity,
maxHeight: 80,
alignment: .top
)
ScrollView(.vertical, showsIndicators: true) {
ForEach(self.games.sorted(by: { $0.players.count > $1.players.count }), id: \.id){ game in
AvailableGame(game: game)
.environmentObject(gameManager)
Divider()
}
} // Scrollview
} // VStack
.padding(25)
// .navigationBarHidden(true)
.navigationTitle("Games").foregroundColor(Color("primary"))
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
HStack {
Button(action: {
self.games = []
Database().fetchGames(){ game in
if let game = game {
self.games.append(contentsOf: game)
}
}
}){
Image(systemName: "arrow.clockwise")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: diameter, height: diameter)
.foregroundColor(.gray)
.padding(.horizontal, 30)
}
NavigationLink(destination: Settings()){
Image("settings")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: diameter, height: diameter)
.foregroundColor(.gray)
}
}.padding(.top, 10)
}
}
} // NavigationView
.sheet(isPresented: $showCreateGameSheet, onDismiss: { }) {
CreateGame()
.environmentObject(self.gameManager)
}
.onAppear {
self.soundManager.playSound(name: "duringGame.aiff", loop: false)
Database().fetchGames(){ game in
if let game = game {
self.games.append(contentsOf: game)
}
}
}
.accentColor(Color("primary"))
}
}
ActiveGame.swift (cuts out after a couple of seconds)
struct ActiveGame: View {
@EnvironmentObject var gameManager: GameManager
let diameter: CGFloat = 50.0
@State var image = Image(systemName: "rectangle")
@ObservedObject var soundManager = SoundManager()
var body: some View {
ZStack {
PlayerBlock()
.padding(.horizontal, 10)
.position(x: (UIScreen.main.bounds.size.width / 2), y: 30)
VStack(spacing: 15) {
ZStack { // Question and answers block
if self.gameManager.game?.question == nil {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
// .scaleEffect(2.0, anchor: .center)
}
VStack {
Text("Round \(self.gameManager.game?.currentRound ?? 1)")
.font(.system(size: 22))
.foregroundColor(Color.gray)
.multilineTextAlignment(.center)
.padding(.bottom, 5)
// QUESTION
Text("\(self.gameManager.game?.question?.text ?? "")")
.font(.custom("Seravek-Bold", size: 28))
.foregroundColor(Color("primary"))
.multilineTextAlignment(.center)
.padding(.horizontal, 30)
.fixedSize(horizontal: false, vertical: true)
// QUESTION IMAGE
(self.gameManager.cachedQuestionImage ?? Image(systemName: "rectangle"))
.resizable()
.scaledToFit()
.aspectRatio(contentMode: .fill)
.frame(height: 200)
// .frame(width: 280, height: 180)
.clipShape(RoundedRectangle(cornerRadius: 10))
.padding(.horizontal, 30)
.opacity(self.gameManager.cachedQuestionImage == nil ? 0.0 : 1.0) // Hide image while it is loading
.onTapGesture {
self.gameManager.deleteGame()
}
// ANSWERS
if 1 > 0 {
MultipleChoice(options: self.gameManager.game?.question?.options ?? [])
} else if (self.gameManager.game?.question?.type == QuestionType.textInput){
TextInput()
}
}.opacity(self.gameManager.game?.question == nil ? 0.0 : 1.0)
.disabled(self.gameManager.game?.question == nil)
// Hide and disable the question block when the next question is loading
}
}.transition(.fadeTransition)
.padding(.top, 40)
}
.onAppear {
print("onAppear")
self.soundManager.playSound(name: "duringGame.aiff", loop: false)
}
.onDisappear {
print("onDisappear")
self.soundManager.player?.stop()
}
}
}
SoundManager.swift - this is the viewmodel that plays the audio
class SoundManager: ObservableObject {
@Published var player: AVAudioPlayer?
func playSound(name: String, loop: Bool){
let path = Bundle.main.path(forResource: name, ofType: nil)
let url = URL(fileURLWithPath: path!)
print("Play URL from name: \(name)")
do {
player = try AVAudioPlayer(contentsOf: url)
if loop {
player?.numberOfLoops = -1 // Loop forever
}
player?.play()
print("Played sound") // Both views print this when the audio plays
} catch {
print("Error playing \(name) sound")
}
}
}
Any idea what the problem is? The SoundManager
is stored as an observed object in both views, so it should live as long as the view is still alive. In ActiveGame, onDisappear()
does not get called, so the view is still alive and should be playing the audio until the end.
Upvotes: 1
Views: 173
Reputation: 205
The first thing I would fix is changing your @ObservedObject wrapper to a @StateObject wrapper. This will prevent deallocation if the view updates at some point in the process of playing the sound. Let me know if that works...
Upvotes: 2