Warning
This proposal assumes that this proposal has been added to the language, since this proposal is in the earliest stage of evolution and feedback for the attached proposal seems to be very positive.
Motivation
Property wrappers are currently limited to type properties. Use in function arguments or even closures is currently not possible leading to friction in development:
// Use in functions as an argument
func takeBinding(count: Binding<Int>) {
count.wrappedValue = 1
// ^~~~~~~~~~~~ Introduces friction
}
// Use as an argument passed to a closure
func closureUsingBinding(
_ block: (Binding<Int>) -> Void
) { ... }
closureUsingBinding { count in
count.wrappedValue = 2
// ^~~~~~~~~~~~ Introduces friction
}
We essentially see the problem property wrappers were introduced to solve in a slightly different form.
Potential Solution
Declarations
We could enable property wrappers in the above cases:
// Use in functions as an argument
func takeBinding(@Binding count: Int) {
count = 1
// 🙂 Natural use
}
// Use as an argument passed to a closure
closureUsingBinding { @Binding count in
count.wrappedValue = 2
// 🙂 Natural use
}
Calling such functions (and closures)
takeBinding(count: $count)
func closureUsingBinding(
_ block: (Binding<Int>) -> Void
) {
block($myCountBinding)
}
Some property wrappers however provide wrapped-property-style initializers: init(wrappedValue:)
. For such initializers convenience functions would be created:
func takeState(@State count: Int = 0) { ... }
takeState()
takeState(count: 1)
takeState(count: State(
wrappedValue: 2
))
Of course, there are more complex cases:
@propertyWrapper
struct Wrapper<Value> {
...
init(foo: String, wrappedValue value: Value) {
...
}
}
func takeWrapper(
@Wrapper(foo: "bar") wrapper: Int = 0
) { ... }
// Calling `takeWrapper`
takeWrapper()
takeWrapper(wrapper: 1)
takeWrapper(wrapper: Wrapper(
foo: "bar",
wrappedValue: 2
))
But sometimes it makes sense for the wrappedValue
not to be automatically provided:
func takeWrapper(
@Wrapper(foo: "bar") wrapper: Int
) { ... }
// Calling `takeWrapper`
takeWrapper(wrapper: 1) // `wrapper` is required
takeWrapper(wrapper: Wrapper(
foo: "bar",
wrappedValue: 2
))
How would this work with mutable property wrappers?
@propertyWrapper
struct Mutable<Value> {
var wrappedValue: Value
}
func takeMutable(@Mutable count: inout Int) {
// ^~~~~
// ✅ Allowed, since count is inout
...
}
func takeMutable(@Mutable count: Int) {
// ^~~~
// ❌ `Mutable` requires that
// count be marked inout.
...
}
Note that property wrappers like State
will work without being mark inout
:
func takeState(@State count: Int) { ... }
That behavior is intentional as State
is actually not mutable. In fact, it's wrappedValue
property is marked get
and nonmutating set
.
Transformations
As in the original proposal the compiler magic lies in the transformations. Currently, this:
struct Foo {
@Mutable var bar: Int
}
is transformed into this:
struct Foo {
var _bar: Mutable<Int>
var bar: Int {
get {
_bar.wrappedValue
}
set {
_bar.wrappedValue = newValue
}
}
}
Likewise, transformation would also occur with this solution:
Simple case
func takeBinding(@Binding count: Int) {
...
}
Becomes:
func takeBinding(count _count: Binding<Int>) {
var count: Int {
get {
_count.wrappedValue
}
nonmutating set {
_count.wrappedValue = newValue
}
}
var $count: Binding<Int> {
_count.projectedValue
}
...
}
Custom labels
func increment(@Binding by count: Int) {
...
}
Becomes:
func takeBinding(by _count: Binding<Int>) {
var count: Int { ... }
var $count: Binding<Int> { ... }
...
}
So what's the behavior with labels?
The label written out by the user becomes the custom label and the property name is prefixed with "_".
Closure arguments
closureUsingBinding { @Binding count in
...
}
Becomes:
closureUsingBinding { (_count: Binding<Int>) -> Void in
var count: Int { ... }
var $count: Binding<Int> { ... }
...
}
Wrapped-value-type initializers
func takeState(
@State count: Int = 0
) { ... }
Becomes:
func takeState(
count _count: State<Int>
) {
...
}
// Synthesized function
func takeState(
count: Int = 0
) {
takeState(count: State<Int>(
wrappedValue: count
))
}
Complex cases
func takeWrapper(
@Wrapper(foo: "bar") wrapper: Int = 0
) { ... }
Becomes:
func takeWrapper(
wrapper _wrapper: Wrapper<Int>
) {
var wrapper: Int { ... }
...
}
// Synthesized function
func takeWrapper(
wrapper: Int = 0
) {
takeWrapper(Wrapper<Int>(
foo: "bar",
wrappedValue: wrapper
))
}
Complex cases with no provided wrappedValue
func takeWrapper(
@Wrapper(foo: "bar") wrapper: Int
) { ... }
Becomes:
func takeWrapper(
wrapper _wrapper: Wrapper<Int>
) {
...
}
// Synthesized function
func takeWrapper(
wrapper: Int // There is no `= 0` here,
// so specifying `wrapper`
// is mandatory
) {
takeWrapper(Wrapper<Int>(
foo: "bar",
wrappedValue: wrapper
))
}