79615780

Date: 2025-05-10 18:09:29
Score: 0.5
Natty:
Report link

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?")
      }
    }
  }
}
Reasons:
  • Long answer (-1):
  • Has code block (-0.5):
  • User mentioned (1): @Environment
  • User mentioned (0): @AppStorage
  • User mentioned (0): @AppStorage
  • Low reputation (1):
Posted by: Tornike Despotashvili