I have an app that will play a confirmation tone if a toggle switch is set. Because the tone may be played for different actions throughout the app, I store the toggle status in the SoundManager class.
I am updating this code from ObservableObject
to @Observable
(there were no errors in the ObservableObject
version). When I build the new code for @Observable
, the error
Cannot find '$sound' in scope
appears at the UtilView
toggle switch. If I remove the dollar sign I get the error
Cannot convert value 'soundStatus' of type 'Bool' to expected type 'Binding', use wrapper instead. Insert $
In either case, I still have an error.
Several examples on Stackoverflow (pre @Observable) with the same error for the binding suggested using .wrappedValue
or .enumerated()
. It doesn't seem like I can use a standard binding since I'm checking the toggle status in multiple places throughout the app.
Here is the piece of code that activated the confirmation tone:
@Environment(SoundManager.self) private var sound
if sound.soundStatus {
sound.playSound(sound: .confirmTone)
}
struct UtilView: View {
@Environment(SoundManager.self) private var sound
var body: some View {
VStack (alignment: .leading) {
List {
// menu here
Toggle(isOn: $sound.soundStatus, // <– Error here
label: { Text("App Sound Confirmations") }
)
.toggleStyle(SwitchToggleStyle())
}
}
.listStyle(.plain)
}
}
This is the soundManager
class:
import AVKit
import SwiftUI
@Observable class SoundManager {
// class SoundManager: ObservableObject {
@AppStorage(StorageKeys.sound.rawValue) var soundStatus: Bool = false
var player: AVAudioPlayer?
var session: AVAudioSession = .sharedInstance()
func playSound(sound: SoundOption) {
guard let url = Bundle.main.url(forResource: sound.rawValue, withExtension: ".wav") else { return }
do {
try session.setActive(false)
try session.setCategory(.playback)
player = try AVAudioPlayer(contentsOf: url)
player?.setVolume( 0.2, fadeDuration: 0)
try session.setActive(true)
player?.play()
}
catch let error {
#if DEBUG
print("Error playing sound. \(error.localizedDescription)")
#endif
}
}
}
The @Observable class SoundManager
cannot observe @AppStorage
directly.
Based on this post Swift: Ist there any way using @AppStorage with @Observable? you could use the following example code.
Note also the important use of @Bindable var sound = sound
to remove the error Cannot find '$sound' in scope
.
See also Managing model data in your app for how to use Bindable
.
struct ContentView: View {
@State private var sound = SoundManager() // <--- here
var body: some View {
UtilView()
.environment(sound) // <--- here
}
}
struct UtilView: View {
@Environment(SoundManager.self) private var sound
var body: some View {
@Bindable var sound = sound // <--- here
VStack (alignment: .leading) {
Text("soundStatus: \(sound.soundStatus)") // <--- for testing
List {
// menu here
Toggle(isOn: $sound.soundStatus,
label: { Text("App Sound Confirmations") }
)
.toggleStyle(SwitchToggleStyle())
}
}
.listStyle(.plain)
}
}
@Observable class SoundManager {
// --- here
var soundStatus: Bool {
get {
access(keyPath: \.soundStatus)
return UserDefaults.standard.bool(forKey: "sound") // StorageKeys.sound.rawValue
}
set {
withMutation(keyPath: \.soundStatus) {
UserDefaults.standard.setValue(newValue, forKey: "sound") // StorageKeys.sound.rawValue
}
}
}
// ....
}