SwiftUI: Applying a color scheme globally at runtime

One of the requirements for an app I'm working on is for the user to be able to choose from a handful of "themes" for their experience. A "theme" in this context is pretty simple:
  • An accentColor

  • A background color (for lists, scroll views, etc)

The intent is for the theme to be applied in inverse to the Navigation Bar, so if the accentColor is a dark blue, the background of the nav bar is dark blue, and the (much lighter blue) foreground color is used as the tint color of the nav bar contents.

I know how to apply these styles appropriately to all the app elements involved. Where I'm having trouble is: When the user goes into settings (within the app) and chooses a new theme, how can I get the new theme to propagate to all existing NavigationViews (including the main "root" navigation, which is "beneath" the settings modal)?

I thought maybe I could use a preference key to bubble the theme choice all the way up to the root view, set the theme, then propagate it back down with an environment key, but I've either got something wrong with the relevant incantation, or that just isn't going to play nice with the appearance() methods necessary to set the NavigationBar background.

Has anyone accomplished something like this in a SwiftUI app?


Replies

Not very sure, but I am guessing you can use something like @AppStorage?
I should have been clearer. Storage of the theme preference isn't an issue (user data, including the theme choice, is stored in a separate persistent storage container). It's how to trigger the invalidation and redrawing of the entire App UI (all views) on the change.
I have something like this setup for instant changes, store the stylesheet as a state variable, and then when you want to change stylesheet, listen for a change.

You can then use @Environment(\.stylesheet) var stylesheet and style your views as you see fit

Code Block @main
struct MyApp: App {
  @State var stylesheet = StylesheetLoader.load(file: "MyStylesheet", in: Bundle.main)
var body: some Scene {
    WindowGroup {
      NavigationView {
        HomeView()
          .environment(\.stylesheet, stylesheet)
.onReceive(stylesheetPublisher, perform: { newStylesheet in
stylesheet = newStylesheet
})
}
}
}


In that example, are you able to affect changes to the NavigationBar of the enclosing NavigationView? That's the root of the problem I'm having. I can get the theme to propagate to most views in the app, but can't seem to make the existing NavigationBars update. If a given modal is dismissed and reopened, it picks up the new theme, but that doesn't help the ones that are on the screen at the time of change, and nothing short of killing and restarting the app seems to cause the MainView's NavigationBar to invalidate.

It's worth noting that I'm using UINavigationBar's appearance proxy to set the nav bar styling. I'm guessing this isn't playing nicely with the SwiftUI view lifecycle, but I can't figure out any other way to style the nav bars.
@karim, Sorry, I haven't tried it with a navigation bar, I've had lots of issues with appearance and navigation bars, I've given up trying to get it working