Button image doesn't change when tapped

I am trying to get the image of the button to change immediately when tapped but at the moment it only changes when tapped and I exit the page and then go back to the page.

Here is the button

VStack {
                Image(systemName: self.soundEffectsM.self.seOn ? "speaker.wave.2.circle": "speaker.slash.circle")
                    .resizable()
                    .frame(width: 43, height: 43)
                    Text(self.soundEffectsM.self.seOn ? "Press to Mute" : "Press to Play")
                        .font(Font.system(size: 15, design: .monospaced))
                        .foregroundColor(.yellow)
                }.foregroundColor(self.soundEffectsM.self.seOn ? .green : .red)
                    .opacity(self.soundEffectsM.self.seOn ? 1 : 1)
                    .padding(.all, 10)
                    .frame(minWidth: 0, maxWidth: .infinity)
                    .background(Color.black.opacity(0.5))
                    .background(LinearGradient(gradient: Gradient(colors: [Color.black, Color.green, Color.black]), startPoint: .topLeading, endPoint: .bottomTrailing))
                    .clipShape(Capsule())
                    .shadow(color: Color.black, radius: 0, y: 3)
                    .shadow(color: Color.gray, radius: 0, y: 1).shadow(color: (Color.black).opacity(0.6), radius: 5, x: 5, y: 5)
                .onTapGesture{self.soundEffectsM.self.seOn.toggle()}

Saving the user default

class soundEffectsModel: ObservableObject {

    @AppStorage("soundEM") var seOn = true
}

Any ideas?

@AppStorage is intended for use in Views. Using it outside of a View, for instance in a view model class, does not work correctly, as you discovered.

You can do this instead if you want to observe a property from your view model while also having it read from/write to UserDefaults:

class SoundEffectsModel: ObservableObject {
    @Published var soundEM: Bool = true {
        didSet {
            UserDefaults.standard.set(soundEM, forKey: "soundEM")
        }
    }
    
    init() {
        soundEM = UserDefaults.standard.bool(forKey: "soundEM")
    }
}

I would suggest, though, taking this property out of your view model and putting it into a separate class just for settings that you pass around through the environment.

And just some friendly advice:

  1. Types should be named starting with a capital letters, so SoundEffectsModel rather than soundEffectsModel. You would use a name that starts with a lower case letter for a property of that type, so var soundEffectsModel: SoundEffectsModel and such.
  2. Name your properties better than seOn. Names should be descriptive and tell anyone reading your code what they are for. soundEM (which you do use for your UserDefaults key) is a much better name than seOn. Spelling out the EM part would be even better. Another example would be to use soundEffectsModel instead of soundEffectsM. (Note that you wouldn’t have had to abbreviate to avoid a name collision if you had named your class SoundEffectsModel in the first place.) There’s no need to be so stingy with names; clarity is to be prized over brevity and code completion means you almost never have to type the whole thing out anyway.

These things will help make your code more readable to others… and yourself later down the road. This makes your code more maintainable. And, heck, it just looks nicer when reading it.

1 Like

Thank you very much. I moved @AppStorage to the View and its all working correctly. I was writing to UserDefaults previously as you have suggested in the example but there was an issue when first opening the app that it wouldn’t play sounds correctly.

Also thank you for taking the time to give me advice on how I can improve my code, I will certainly be applying it moving forward.