Alert structure and playing sound

Hello, I'm working on an SwiftUI iOS app that shows a list of timers. When the timer is up then I pop up an alert struct. The user hits "ok" to dismiss the alert. I am trying to include an alarm sound using AVFoundation. I can get the sounds to play if I change the code to play when a button clicks so I believe I have the url path correct. But I really want it to play during the alert pop up. I have not been able to find examples where this is done using an alert so I suspect I need a custom view but thought I'd try the alert route first. Anyone try this before?

@State var audioPlayer: AVAudioPlayer?
.alert(isPresented: $showAlarmAlert) {
playSound() -- Calls AVFoundation
return Alert(title: Text("Time's Up!"))
}
func playSound() {
let alertSoundPath = Bundle.main.url(forResource: "classicAlarm", withExtension: "mp3")!
do {
audioPlayer = try AVAudioPlayer(contentsOf: alertSoundPath)
audioPlayer?.play()
}
catch {
appData.logger.debug("Error playing sound: \(alertSoundPath)")
}
}
Answered by Claude31 in 835980022

There is effectively a problem, no sound.

I tested with to different calls:

func playInputClick() {
var filePath: String?
filePath = Bundle.main.path(forResource: "ButtonTap", ofType: "wav")
let fileURL = URL(fileURLWithPath: filePath!)
var soundID:SystemSoundID = 0
AudioServicesCreateSystemSoundID(fileURL as CFURL, &soundID)
AudioServicesPlaySystemSound(soundID)
}
func playSound() {
let alertSoundPath = URL(fileURLWithPath: Bundle.main.path(forResource: "ButtonTap", ofType: "wav")!) // Bundle.main.url(forResource: "classicAlarm", withExtension: "mp3")!
do {
let audioPlayer = try AVAudioPlayer(contentsOf: alertSoundPath)
audioPlayer.play()
}
catch {
// appData.logger.debug("Error playing sound: \(alertSoundPath)")
}
}:

playSound does not work, while playInputClick() does.

So, I would change your playSound as follows:

func playSound() {
let alertSoundPath = Bundle.main.path(forResource: "classicAlarm", ofType: "mp3")!
let alertSoundURL = URL(fileURLWithPath: alertSoundPath)
var soundID:SystemSoundID = 0
AudioServicesCreateSystemSoundID(alertSoundURL as CFURL, &soundID)
AudioServicesPlaySystemSound(soundID)
}

In my tests, func are outside ContentView.

I moved playSound inside ContentView and got the runtime error (but some sound):

Modifying state during view update, this will cause undefined behavior.

So, please, show complete code so that we can reproduce (as well as the resources).

Accepted Answer

There is effectively a problem, no sound.

I tested with to different calls:

func playInputClick() {
var filePath: String?
filePath = Bundle.main.path(forResource: "ButtonTap", ofType: "wav")
let fileURL = URL(fileURLWithPath: filePath!)
var soundID:SystemSoundID = 0
AudioServicesCreateSystemSoundID(fileURL as CFURL, &soundID)
AudioServicesPlaySystemSound(soundID)
}
func playSound() {
let alertSoundPath = URL(fileURLWithPath: Bundle.main.path(forResource: "ButtonTap", ofType: "wav")!) // Bundle.main.url(forResource: "classicAlarm", withExtension: "mp3")!
do {
let audioPlayer = try AVAudioPlayer(contentsOf: alertSoundPath)
audioPlayer.play()
}
catch {
// appData.logger.debug("Error playing sound: \(alertSoundPath)")
}
}:

playSound does not work, while playInputClick() does.

So, I would change your playSound as follows:

func playSound() {
let alertSoundPath = Bundle.main.path(forResource: "classicAlarm", ofType: "mp3")!
let alertSoundURL = URL(fileURLWithPath: alertSoundPath)
var soundID:SystemSoundID = 0
AudioServicesCreateSystemSoundID(alertSoundURL as CFURL, &soundID)
AudioServicesPlaySystemSound(soundID)
}

In my tests, func are outside ContentView.

I moved playSound inside ContentView and got the runtime error (but some sound):

Modifying state during view update, this will cause undefined behavior.

So, please, show complete code so that we can reproduce (as well as the resources).

Hi Claude31,

Thank you for your reply. Your version of playSound worked for me. I'm not sure why the SystemSound option works while AVAudioPlayer does not. I might do some reading on that.

Thanks, MGeek.

My app has multiple timers and I'm getting strange sound behavior. I think I'll try putting a single sound player in the shared application data instead of a view.

I realized I never got back to this post to show what I ended up with. Like I mentioned before, I was thinking of putting the audio player in shared application data. It looks like this ...

import SwiftUI
import Observation
import os
@preconcurrency import AVFoundation
@main
struct Chain_TimerApp: App {
@State private var appData = ApplicationData.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(appData)
}
}
}
@Observable class ApplicationData: @unchecked Sendable {
var timers: [TimerData] = []
var timerRunningStates: [UUID: Bool] = [:]
var isSerial: Bool = false
var audioData: AudioData
let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "Chain Timer", category: "ApplicationData")
static let shared: ApplicationData = ApplicationData()

. . . For the AudioData structure

struct AudioData {
var audioPlayer: AVAudioPlayer
var sound: String = "classicAlarm"
var logger: Logger
init(logger: Logger) {
self.logger = logger
audioPlayer = AVAudioPlayer()
}
mutating func setSound(sound: String) {
let soundPath = Bundle.main.url(forResource: sound, withExtension: "mp3")! // Default
do {
try audioPlayer = AVAudioPlayer(contentsOf: soundPath)
} catch {
logger.error("Cannot create player. Error: \(error)")
}
}
func playSound() {
if !audioPlayer.isPlaying { // Only play one at a time
audioPlayer.play()
}
}
func cancelSound() {
audioPlayer.stop()
audioPlayer.currentTime = 0 // Reset playback
}
}

Then in my view, I grab application data from the environment and add an alert modifier to a Text view that will use the audio player ...

@Environment(ApplicationData.self) private var appData
.alert(isPresented: $showAlarmAlert) {
appData.audioData.playSound()
return Alert(title: Text("Time's Up!"),
message: Text("Hit Okay"),
dismissButton: .cancel(Text("Okay"), action: {
appData.audioData.cancelSound()

Putting the audio player in shared data lets any view make use of it.

Thanks, MGeek

Alert structure and playing sound
 
 
Q