skip to Main Content

I am making a game with a minimal color scheme. I store the colors that I use as computed static var‘s in an enum so that I can call them in any view. I am trying to make a secondary color scheme (colorblind). My code looks like this:

enum GameColors {
    static var exampleColor: Color {
        !UserDefaults.standard.bool(forKey: GamePrefs.colorBlindMode) ? Color.green : Color(red: 0 / 255, green: 213 / 255, blue: 255 / 255)
    }
}
enum GamePrefs {
     static let colorBlindMode: String = "colorBlindMode"
}

My settings menu is called in my main menu view like so:

struct MainMenuView: View {
    @State var settingsClicked: Bool = false

    var body: some View {
        VStack {
            Button {
                settingsClicked.toggle()
            } label: {
                Text("Settings")
                .foregroundColor(GameColors.exampleColor)
            }
            if settingsClicked {
                SettingsView()
            }
        }
    }
}

struct SettingsView: View {
    @AppStorage(GamePrefs.colorBlindMode) var colorBlindMode = false
    var body: some View {
        Toggle(isOn: $colorBlindMode) {
            Text("Color Blind Mode: (colorBlindMode ? "On" : "Off")")
                .foregroundColor(GameColors.exampleColor)
        }
    }
}

When I toggle colorBlindMode, only SettingsView‘s colors are updated, the color of the main menu does not change unless I interact with it. How do I get both to update?

I’ve tried binding the @AppStorage property wrapper to no success.

Example

3

Answers


  1. Chosen as BEST ANSWER

    After reading a bit more about what causes SwiftUI to reload a view, and a whole lot of testing different methods, I believe that the simplest way to solve this problem is to introduce a new @State variable which you must use somewhere in the parent view:

    struct MainMenuView: View {
        @State var settingsClicked: Bool = false
        @State var reloadView: Bool = false        // <-- here
    
        var body: some View {
            VStack {
                Button {
                    settingsClicked.toggle()
                } label: {
                    Text("Settings")
                    .foregroundColor(GameColors.exampleColor)
                }
                if settingsClicked {
                    SettingsView(reloadParentView: $reloadView)
                }
                if reloadView {}                   // <-- here
            }
        }
    }
    
    struct SettingsView: View {
        @Binding var reloadParentView: Bool        // <-- here
        @AppStorage(GamePrefs.colorBlindMode) var colorBlindMode = false
        var body: some View {
            Toggle(isOn: $colorBlindMode) {
                Text("Color Blind Mode: (colorBlindMode ? "On" : "Off")")
                    .foregroundColor(GameColors.exampleColor)
            }
            .onChange(of: colorBlindMode) { _ in
                reloadParentView.toggle()        // <-- here
            }
        }
    }
    

    Passing this state from the parent view to the child view allows the child to force reloads on the parent at will.


  2. You could try this approach, using @AppStorage and .onReceive in MainMenuView,
    to ensure GameColors.exampleColor is updated/received in the MainMenuView while
    clicking in the SettingsView.

    struct MainMenuView: View {
        @AppStorage(GamePrefs.colorBlindMode) var colorBlindMode = false  // <-- here
        @State var settingsClicked: Bool = false
        
        var body: some View {
            VStack {
                Button {
                    settingsClicked.toggle()
                } label: {
                    Text("Settings").foregroundColor(GameColors.exampleColor)
                }
                if settingsClicked {
                    SettingsView() 
                }
            }
            .onReceive(Just(colorBlindMode)) { _ in  // <-- here
            }
        }
    }
    
    struct SettingsView: View {
        @AppStorage(GamePrefs.colorBlindMode) var colorBlindMode = false
        
        var body: some View {
            Toggle(isOn: $colorBlindMode) {
                Text("Color Blind Mode: (colorBlindMode ? "On" : "Off")")
                    .foregroundColor(GameColors.exampleColor)  // <-- here for testing
            }
            .toggleStyle(SwitchToggleStyle(tint: GameColors.exampleColor))
        }
    }
    
    Login or Signup to reply.
  3. The reason your color does not change is because of your MainMenuView is not refreshing when you press the toggle. Only your SettingsView is refreshed. So you would need something to notify your MainMenuView that something has changed.

    To do so you can add another AppStorage to your MainMenuView and force your view to refresh by creating a dependency on this property.

    struct MainMenuView: View {
        @State var settingsClicked: Bool = false
        // add the same AppStorage as in SettingsView
        @AppStorage(GamePrefs.colorBlindMode) var colorBlindMode = false
        var body: some View {
            VStack {
                Button {
                    settingsClicked.toggle()
                } label: {
                    let _ = print("text")
                    Text("Settings")
                        //this will force the view to update
                        .foregroundColor(colorBlindMode ? GameColors.exampleColor : GameColors.exampleColor)
                }
                if settingsClicked {
                    SettingsView()
                }
            }
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search