You’re encountering an issue because you’re trying to use your SoundManager from child views via @Environment, but you’re not injecting it from the parent view. Here’s what you should do to fix and improve the structure:
Since SoundManager is marked as @Observable
, you need to pass it down using the .environment(\_:)
modifier from your root view (ContentView). Otherwise, the child views won’t be able to access it and might crash at runtime.
.environment(soundManager)
Rather than having SoundManager read from UserDefaults or @AppStorage internally (which can cause timing issues or stale values), it’s more reliable to read the toggle value directly from the view using @AppStorage, and pass it to playSound() as a parameter:
try soundManager.playSound(sound: .confirmTone, soundStatus: soundStatus)
This makes your code more predictable and avoids syncing issues with persistent storage.
import AVKit
import SwiftUI
import Observation
@Observable
final class SoundManager {
var player: AVAudioPlayer?
var session: AVAudioSession = .sharedInstance()
enum SoundOption: String {
case posSound
case confirmTone
case positiveTone
}
// use here your toggle state from @AppStorage and pass as parameter
func playSound(sound: SoundOption, soundStatus: Bool) throws {
if soundStatus {
guard let url = Bundle.main.url(forResource: sound.rawValue, withExtension: ".wav") else { return }
do {
try session.setActive(true)
try session.setCategory(.playback)
player = try AVAudioPlayer(contentsOf: url)
try session.setActive(false)
player?.play()
} catch let error {
throw error
}
}
}
}
struct ContentView: View {
@Bindable var soundManager: SoundManager
@AppStorage("toggleStorage") var soundStatus = false
init(soundManager: SoundManager = SoundManager()) {
self.soundManager = soundManager
}
var body: some View {
NavigationStack {
VStack (alignment: .leading) {
List {
NavigationLink("Buy Tomatoes", destination: Feature1())
NavigationLink("Buy Potatoes", destination: Feature2())
Toggle(isOn: $soundStatus,
label: { Text("App Sound Confirmations") }
)
}
}
}
.environment(soundManager) //pass environment from here to your child views
}
}
struct Feature1: View {
@Environment(\.dismiss) var dismiss
@Environment(SoundManager.self) private var soundMgr
@AppStorage("toggleStorage") var soundStatus = false // Read toggle value directly from AppStorage here
@State private var error: Error?
var body: some View {
VStack {
Text("Feature 1")
.font(.title)
Button {
do {
// Pass soundStatus to the playSound function
try soundMgr.playSound(sound: .posSound, soundStatus: soundStatus)
} catch {
self.error = error
}
dismiss()
} label: {
Text("Purchase Tomatoes?")
}
}
}
}
struct Feature2: View {
@Environment(\.dismiss) var dismiss
@Environment(SoundManager.self) private var soundMgr
@AppStorage("toggleStorage") var soundStatus = false // Read toggle value directly from AppStorage here
@State private var error: Error?
var body: some View {
VStack {
Text("Feature 2")
.font(.title)
Button {
do {
// Pass soundStatus to the playSound function
try soundMgr.playSound(sound: .posSound, soundStatus: soundStatus)
} catch {
self.error = error
}
dismiss()
} label: {
Text("Purchase Potatoes?")
}
}
}
}