I’m trying to pass a @Binding value down the SwiftUI view hierarchy using a custom @EnvironmentKey. In my NestedView, I’m using a combination of @Environment and @Binding like this:
@Environment(\.featureEnabledBinding) @Binding var myBool: Bool
I should be able to use this in a Toggle as:
Toggle(isOn: $myBool, label: { Text("Toggle") })
However, this gives me a compiler error:
Cannot find '$myBool' in scope
this works just fine:
Toggle(isOn: _myBool.wrappedValue, label: { Text("Toggle") })
This is the opposite of what I expected. From what I understand: • _myBool should be the wrapper (Binding) • $myBool should be the projected value (also a Binding)
Here is my code
Environment Setup:
struct FeatureEnabledKey: EnvironmentKey {
static let defaultValue: Binding<Bool> = .constant(false)
}
extension EnvironmentValues {
var featureEnabledBinding: Binding<Bool> {
get { self[FeatureEnabledKey.self] }
set { self[FeatureEnabledKey.self] = newValue }
}
}
and Minimal Example
struct ParentView: View {
@State private var isFeatureEnabled = false
var body: some View {
NestedView()
.environment(\.featureEnabledBinding, $isFeatureEnabled)
}
}
struct NestedView: View {
@Environment(\.featureEnabledBinding) @Binding var myBool: Bool
var body: some View {
VStack {
// ❌ This gives an error : Cannot find '$myBool' in scope
// Toggle(isOn: $myBool, label: { Text("Toggle") })
// ✅ This works fine
Toggle("Working Toggle", isOn: _myBool.wrappedValue)
}
}
}
Question: Why is $myBool unavailable or invalid in this context? Is this a known SwiftUI/compiler bug with double property wrappers (@Environment @Binding)? or my understandings are not correct
Xcode : 16.3
This is by design. From the property wrappers proposal,
When multiple property wrappers are applied to a given property, only the outermost property wrapper's
projectedValue
will be considered.
So $myBool
would have accessed the projected value of the Environment
(i.e. _myBool.projectedValue
). But since the Environment
property wrapper does not have a projectedValue
, the $myBool
property is not generated.
A similar case is the built-in editMode
, or the now-deprecated presentationMode
. I prefer to not write Binding
as a property wrapper, and write it as part of the property type instead.
@Environment(\.featureEnabledBinding) var myBool: Binding<Bool>
Then myBool
would be a Binding<Bool>
, myBool.wrappedValue
would be the Bool
.
Also consider doing what editMode
does - change the environment value's type to Binding<Bool>?
, and change the default value to nil
. This makes it obvious if you forgot to set the environment value.