When using SwiftUI it is common to pass a binding to a subview. Usually, a change to that binding's value will trigger a change to the UI, and often we want that change to be animated. For this reason, we've been given this tool:
Subview(binding: $myValue.animation())
This will apply an animation to any change to the binding. While this is a nice feature, I have found that I end up with code duplication when I use it - I often pass the same binding to several subviews, and I apply the .animation()
method to all of them. When I decide that I want to use .animation(.spring())
instead, I have to change it everywhere or I end up with inconsistent animations.
So far I've been using this sub-par solution:
@State
private var isSheetDisplayed = false
private var isSheetDisplayed_modifiedBinding: Binding<Bool> {
$isSheetDisplayed.animation()
}
and then passing isSheetDisplayed_modifiedBinding
to my subviews instead of $isSheetDisplayed
. The advantage of this is of course that if I decide to change the animation (or any other modifiers to the binding) I can do it in one place... theoretically. The downsides are numerous. Perhaps most importantly, it doesn't actually keep me much safer - I still have to remember to use the horribly unergonomic isSheetDisplayed_modifiedBinding
as opposed to the immensely preferable syntax $isSheetDisplayed
, and if I forget and accidentally pass $isSheetDisplayed
to one of my subviews then again I end up with inconsistent animations.
I have a much better solution that seems to work perfectly in all ways except one - it doesn't work... here's how my solution would look in action:
@State.ModifiedBinding({
$0.animation()
})
private var isSheetDisplayed = false
This new property wrapper is defined like this:
extension State where Value == Never {
@propertyWrapper
struct ModifiedBinding <Value> {
@State<Value>
private var state: Value
private let bindingModifier: (Binding<Value>)->Binding<Value>
init (wrappedValue: Value, _ bindingModifier: @escaping (Binding<Value>)->Binding<Value>) {
self._state = State<Value>(initialValue: wrappedValue)
self.bindingModifier = bindingModifier
}
var wrappedValue: Value {
get { state }
set { state = newValue }
}
var projectedValue: Binding<Value> {
bindingModifier($state)
}
}
}
This solution meets all my needs - it allows me to bake certain behavior into the @State
itself so that I can still use the $isSheetDisplayed
syntax - there is no possibility of programmer error here. Sadly, values which are written to the binding that this property wrapper produces do not make it all the way to the source of truth. The binding's setter is indeed invoked with the proper value, but the true underlying @State
is apparently not ultimately changed, because the UI doesn't update and when I query the state directly it always returns the initial value.
The real underlying mystery here is, how exactly does SwiftUI figure out how to map State
values (which are indeed value types) to the underlying persistently referenced memory which they manage? Clearly, my binding here is writing its values to some State
- it just appears that the State
which it is writing to has not been linked up to the underlying persistent value which SwiftUI is retaining.
UPDATE:
The fully functional code, thanks to Adrian Meister's insight, is this:
extension State where Value == Never {
@propertyWrapper
struct ModifiedBinding <Value>: DynamicProperty {
@State<Value>
private var state: Value
private let bindingModifier: (Binding<Value>)->Binding<Value>
init (wrappedValue: Value, _ bindingModifier: @escaping (Binding<Value>)->Binding<Value>) {
self._state = State<Value>(initialValue: wrappedValue)
self.bindingModifier = bindingModifier
}
var wrappedValue: Value {
get { state }
set { state = newValue }
}
var projectedValue: Binding<Value> {
bindingModifier($state)
}
}
}