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.
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.