Update:
This question is already solved (see responses below). The correct way to do this is to get your
Binding
by projecting theObservableObject
For example,$options.refreshRate
.
TLDR version:
How do I get a SwiftUI Picker
(or other API that relies on a local Binding
) to immediately update my ObservedObject
/EnvironmentObject
. Here is more context...
The scenario:
Here is something I consistently need to do in every SwiftUI app I create...
- I always make some class that stores any user preference (let's call this class
Options
and I make it anObservableObject
. - Any setting that needs to be consumed is marked with
@Published
- Any view that consumes this brings it in as a
@ObservedObject
or@EnvironmentObject
and subscribes to changes.
This all works quite nicely. The trouble I always face is how to set this from the UI. From the UI, here is usually what I'm doing (and this should all sound quite normal):
- I have some SwiftUI view like
OptionsPanel
that drives theOptions
class above and allows the user to choose their options. - Let's say we have some option defined by an enum:
enum RefreshRate {
case low, medium, high
}
Naturally, I'd choose a Picker
in SwiftUI to set this... and the Picker
API requires that my selection param be a Binding
. This is where I find the issue...
The issue:
To make the Picker
work, I usually have some local Binding
that is used for this purpose. But, ultimately, I don't care about that local value. What I care about is immediately and instantaneously broadcasting that new value to the rest of the app. The moment I select a new refresh rate, I'd like immediately know that instant about the change. The ObservableObject
(the Options
class) object does this quite nicely. But, I'm just updating a local Binding
. What I need to figure out is how to immediately translate the Picker
's state to the ObservableObject
every time it's changed.
I have a solution that works... but I don't like it. Here is my non-ideal solution:
The non-ideal solution:
The first part of the solution is quite actually fine, but runs into a snag...
Within my SwiftUI view, rather than do the simplest way to set a Binding
with @State
I can use an alternate initializer...
// Rather than this...
@ObservedObject var options: Options
@State var refreshRate: RefreshRate = .medium
// Do this...
@ObservedObject var options: Options
var refreshRate: Binding<RefreshRate>(
get: { self.options.refreshRate },
set: { self.options.refreshRate = $0 }
)
So far, this is great (in theory)! Now, my local Binding
is directly linked to the ObservableObject
. All changes to the Picker
are immediately broadcast to the entire app.
But this doesn't actually work. And this is where I have to do something very messy and non-ideal to get it to work.
The code above produces the following error:
Cannot use instance member 'options' within property initializer; property initializers run before 'self' is available
Here my my (bad) workaround. It works, but it's awful...
The Options
class provides a shared
instance as a static property. So, in my options panel view, I do this:
@ObservedObject var options: Options = .shared // <-- This is still needed to tell SwiftUI to listen for updates
var refreshRate: Binding<RefreshRate>(
get: { Options.shared.refreshRate },
set: { Options.shared.refreshRate = $0 }
)
In practice, this actually kinda works in this case. I don't really need to have multiple instances... just that one. So, as long as I always reference that shared instance, everything works. But it doesn't feel well architected.
So... does anyone have a better solution? This seems like a scenario EVERY app on the face of the planet has to tackle, so it seems like someone must have a better way.
(I am aware some use an .onDisapear
to sync local state to the ObservedObject
but this isn't ideal either. This is non-ideal because I value having immediate updates for the rest of the app.)