Home > Software design >  SwiftUI: Update parent view on change of UserDefaults in child view
SwiftUI: Update parent view on change of UserDefaults in child view

Time:08-24

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

CodePudding user response:

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))
    }
}

CodePudding user response:

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()
            }
        }
    }
}

CodePudding user response:

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.

  • Related