For a simple value like this, particularly a preference selected by the user, the default place to store it would be in the UserDefaults. It's possible (and indeed easy) to write a @UserDefaults property wrapper to handle this, but the issue then is getting it wired into SwiftUI state—your view updates only because you're using an @State variable, and you can't use more than one property wrapper, sadly.
The way I've tackled this sort of bifurcation before is using a little Combine magic. You can use a PassthroughSubject to feed values into as many different places as you'd like, including into an @State value or similar, but it takes a little setup work:
struct CardTheme: View {
/// State variable used to update UI.
@State private var theme: Theme = UserDefaults.standard.object(forKey: "Theme") as? Theme ?? defaultTheme
/// Publisher used to change the value.
private var themeSubject = PassthroughSubject<theme, never="">()
/// Bucket holding on to the subscribers for the `themeSubject` publisher.
private var subscribers: Set = []
init() {
// Attach something to write new values into UserDefaults.
themeSubject
.sink { UserDefaults.standard.set($0, forKey: "Theme") }
.store(in: &subscribers)
// Attach something to write new values into the @State variable.
themeSubject
.assign(to: \.theme, on: self)
.store(in: &subscribers)
}
var body: some View {
Button("Touch me") {
// sends a new value through the publisher into UserDefaults and State.
self.themeSubject.send(Theme.someValue)
}
}
}
An alternative approach with less boilerplate code to write would be to use @ObservedObject and @Published. For this to work, your preference would need to be in a class type, not a struct. Basically you'd create an object like this:
class GlobalPrefs: ObservableObject {
static let shared = GlobalPrefs()
@Published var theme: Theme = UserDefaults.standard.object(forKey: "Theme") as? Theme ?? defaultTheme
/// Users must use `GlobalPrefs.shared` to get the singleton instance.
private init() {}
}
struct CardTheme: View {
@ObservedObject var prefs = GlobalPrefs.shared
private var defaultsSubscriber: AnyCancellable? = nil
init() {
/// The $-prefix on a @Published property returns a Publisher to which you can subscribe.
defaultsSubscriber = prefs.$prefs.sink {
UserDefaults.standard.set($0, forKey: "Theme")
}
}
}
Using @ObservedObject in this way lets SwiftUI know when you access its contents, similar to the way @State works. Thus, in your code, you just do something like this and it'll all work:
if prefs.theme == .someValue {
// do something
}
Button("Set Theme X") {
self.prefs.theme = Theme.someValue
}