Reputation: 2747
I'm new to SwiftUI and still learning so this maybe obvious. The difference between the two is changing from @State to @Binding. There is clearly magic happening that was working for the first case but not working for the second case. Any insights greatly received.
Compiles fine:
struct PointEditorView: View {
@State var sliderPosition: ClosedRange<Float> = 3...8
var body: some View {
Text("PointEditorView")
}
}
Fails to compile:
struct PointEditorView: View {
@Binding var sliderPosition: ClosedRange<Float> = 3...8
var body: some View {
Text("PointEditorView")
}
}
This second case results in two errors:
Upvotes: 1
Views: 132
Reputation: 273540
As far as the language is concerned, this is just how property wrappers (@State
, @Binding
and others) work.
@Foo var foo: SomeType = bar
declares 2 or 3 properties all at once. It lowers to:
private var _foo = Foo(wrappedValue: bar)
var foo: SomeType {
get { _foo.wrappedValue }
set { _foo.wrappedValue = newValue }
}
// only when Foo has a projectedValue property
var $foo: AnotherType {
get { _foo.projectedValue }
set { _foo.projectedValue = newValue }
}
A stored property storing an instance of the property wrapper itself, and computed properties wrapping wrappedValue
and projectedValue
.
Notice how the stored property is initialised. This requires the property wrapper to have an initialiser taking a single parameter, with the parameter label wrappedValue:
. @State
has such an initialiser. @Binding
does not.
@Binding
does have an initialiser with a single parameter label projectedValue:
, taking another Binding
. That's why the confused compiler emits errors saying the parameter label is incorrect, and that the argument cannot be converted to Binding
.
If you remove the initialiser from the declaration:
@Binding var sliderPosition: ClosedRange<Float>
Then _sliderPosition
will not be initialised with Binding(wrappedValue: ...)
(which is invalid), and so there is no problem. This generates a memberwise initialiser that takes a Binding
and initialises _sliderPosition
to the initialiser parameter.
Recommended reading: Initialization of synthesized storage properties section of the Swift Evolution Proposal for property wrappers.
From a SwiftUI point of view, Binding
represents a "binding" to some source of truth (or another Binding
), like a @State
property, a @Published
property of an ObservableObject
, etc. A @Binding
property is designed to be uninitialised, so that parent views can make a Binding
that binds to some state of the parent, and pass it to the child.
If you want a Binding
that wraps a constant value, use Binding.constant(...)
. You do not need the property wrapper.
let sliderPosition: Binding<ClosedRange<Float>> = .constant(3...8)
Upvotes: 0
Reputation: 220
The issue with your second piece of code is because @Binding
works differently than @State
. While @State
creates and owns the data, @Binding
does not - its meant to be a reference to data owned by another part of your app, in your case the PointEditorView
is the owner
To fix the second example, you need to pass the sliderPosition as a Binding from a parent view, like this:
struct ParentView: View {
@State private var sliderPosition: Float = 3.0
var body: some View {
PointEditorView(sliderPosition: $sliderPosition)
}
}
struct PointEditorView: View {
@Binding var sliderPosition: Float
var body: some View {
VStack {
Text(sliderPosition.description)
Slider(value: $sliderPosition, in: 3...8)
}
}
}
Here, ParentView
owns the state and passes a binding reference to PointEditorView. I placed it in a VStack so that we can vertically stack the Views and see the changes.
Upvotes: 0