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
.
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...
Here is something I consistently need to do in every SwiftUI app I create...
Options
and I make it an ObservableObject
.@Published
@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):
OptionsPanel
that drives the Options
class above and allows the user to choose their options.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...
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 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.)
The good news is you're trying way, way, way too hard.
The ObservedObject
property wrapper can create this Binding
for you. All you need to say is $options.refreshRate
.
Here's a test playground for you to try out:
import SwiftUI
enum RefreshRate {
case low, medium, high
}
class Options: ObservableObject {
@Published var refreshRate = RefreshRate.medium
}
struct RefreshRateEditor: View {
@ObservedObject var options: Options
var body: some View {
// vvvvvvvvvvvvvvvvvvvv
Picker("Refresh Rate", selection: $options.refreshRate) {
// ^^^^^^^^^^^^^^^^^^^^
Text("Low").tag(RefreshRate.low)
Text("Medium").tag(RefreshRate.medium)
Text("High").tag(RefreshRate.high)
}
.pickerStyle(.segmented)
}
}
struct ContentView: View {
@StateObject var options = Options()
var body: some View {
VStack {
RefreshRateEditor(options: options)
Text("Refresh rate: \(options.refreshRate)" as String)
}
.padding()
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(ContentView())
It's also worth noting that if you want to create a custom Binding
, the code you wrote almost works. Just change it to be a computed property instead of a stored property:
var refreshRate: Binding<RefreshRate> {
.init(
get: { self.options.refreshRate },
set: { self.options.refreshRate = $0 }
)
}