SwiftUI: using colorScheme within EnvironmentObject

1k Views Asked by At

In our app, we have a basic colour palette used for the main design, however there is also the possibility for the user to choose their own colours via an admin system (for white labelling purposes). These colours are returned to us as hex strings and stored in a BusinessProfile object using the following model:

struct BusinessProfileColors: Codable {
    let success: BusinessProfileColor?
    let warning: BusinessProfileColor?
    let highlight: BusinessProfileColor?
    // ... more colours here
}

struct BusinessProfileColor: Codable {
    let light: String?
    let dark: String?
}

So I am trying to create a class which will check for the presence of valid hex colours in the BusinessProfile and use these (with the correct light/dark mode colours selected according to the app state). If the colour value for a specific palette is nil or not a valid hex string, we will fall back on the standard design colours. Here is how the class looks:

class ColorPalette: ObservableObject {
    @Environment(\.colorScheme) var colorScheme
    let container: DIContainer
    
    init(container: DIContainer) {
        self.container = container
    }
    
    private func dynamicColor(lightColor: Color?, darkColor: Color?, defaultColor: Color) -> Color {
        switch colorScheme {
        case .dark:
            return darkColor ?? lightColor ?? defaultColor
        default:
            return lightColor ?? defaultColor
        }
    }
    
    var alertSuccess: Color {
        return dynamicColor(
            lightColor: Color(hex: container.appState.value.businessData.businessProfile?.colors?.success?.light),
            darkColor: Color(hex: container.appState.value.businessData.businessProfile?.colors?.success?.dark),
            defaultColor: Color("Success"))
    }

    // More colours follow the same pattern
}

The idea is that I want to pass this to my views as an @EnvironmentObject like this:

@EnvironmentObject var colorPalette: DynamicColors

So that within these views I can declare the colours I want simply like e.g.:

text
.foregroundColor(colorPalette.alertSuccess)

So I am declaring an instance of this class in my first parent view (RootView.swift) as an @StateObject var as follows:

@StateObject var colorPalette: DynamicColors

So when I first initialise RootView it is like this:

RootView(viewModel: RootViewModel(container: viewModel.environment.container), colorPalette: .init(container: viewModel.environment.container))
            

(NB: the viewModel.environemnt.container is where we store the BusinessProfile).

Then I am passing this as an environment object from the RootView to the first view where I need the colours like this:

TabBarView(viewModel: .init(container: viewModel.container))
                .environmentObject(colorPalette)

However, in TabBarView, whilst the colours from the API results are returning and being displayed, the light/dark mode switch does nothing. Only the light mode colours are showing.

If I change the setup so that I declare the ColorPalette individually on each view and pass in the colorScheme from here all works fine, e.g.:

struct SomeView: View {
    @Environment(\.colorScheme) var colorScheme
    let container: DIContainer
    
    var colorPalette: ColorPalette {
      ColorPalette(colorScheme: colorScheme, container: container) // Pass in the color scheme from the current view and use this in the ColorPalette class
    }
}

However, this seems extremely heavy handed and I would much rather use @EnvironmentObject as it seems well suited to this scenario. However, how can I leverage the @Environment(\.colorScheme) property in this scenario from within my ColorPalette class?

0

There are 0 best solutions below