I have recently been experimenting with swift property wrappers and wondered if there was any way of composing them together in order to achieve a more modular architecture. E.g:
@WrapperOne @WrapperTwo var foo: T
Looking through the documentation yielded nothing. The only reference to how to do this is on this GitHub page (https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md) (quotes below) which seems to say it is possible. Other articles have said that they are difficult to compose but have not explained how to go about doing this. However, I can't make heads or tails of it and would appreciate it if someone could show me some example code on how to implement this (see bottom of post).
When multiple property wrappers are provided for a given property, the wrappers are composed together to get both effects. For example, consider the composition of
DelayedMutable
andCopying
:@DelayedMutable @Copying var path: UIBezierPath
Here, we have a property for which we can delay initialization until later. When we do set a value, it will be copied via
NSCopying
's copy method. Composition is implemented by nesting later wrapper types inside earlier wrapper types, where the innermost nested type is the original property's type. For the example above, the backing storage will be of typeDelayedMutable<Copying<UIBezierPath>>
and the synthesized getter/setter for path will look through both levels of .wrappedValue:private var _path: DelayedMutable<Copying<UIBezierPath>> = .init() var path: UIBezierPath { get { return _path.wrappedValue.wrappedValue } set { _path.wrappedValue.wrappedValue = newValue } }
Note that this design means that property wrapper composition is not commutative, because the order of the attributes affects how the nesting is performed: @DelayedMutable @Copying var path1: UIBezierPath // _path1 has type DelayedMutable> @Copying @DelayedMutable var path2: UIBezierPath // error: _path2 has ill-formed type Copying> In this case, the type checker prevents the second ordering, because
DelayedMutable
does not conform to theNSCopying
protocol. This won't always be the case: some semantically-bad compositions won't necessarily by caught by the type system. Alternatives to this approach to composition are presented in "Alternatives considered."
Ideally, I would like to implement something like the following:
@propertyWrapper
struct Doubled {
var number: Int
var wrappedValue: Int {
get { (value * 2) }
set { value = Int(newValue / 2) }
}
init(wrappedValue: Int) {
self.number = wrappedValue
}
}
and
@propertyWrapper
struct Tripled {
var number: Int
var wrappedValue: Int {
get { (value * 3) }
set { value = Int(newValue / 3) }
}
init(wrappedValue: Int) {
self.number = wrappedValue
}
}
so that this could be achieved:
@Tripled @Doubled var number = 5
I understand that this example is a somewhat silly reason to implement property wrapper composition but this is merely for simplicity's sake when learning a new feature.
Any help would be greatly appreciated.
To make our wrapper configurable, we need to add all configuration parameters through an initializer. If the initializer contains a wrappedValue attribute (the initial value of our property), it must be the first parameter.
A property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property. For example, if you have properties that provide thread-safety checks or store their underlying data in a database, you have to write that code on every property.
A projection of the binding value that returns a binding. Returns a binding to the resulting value of a given key path.
Property observers observe and respond to changes in a property's value. Property observers are called every time a property's value is set, even if the new value is the same as the property's current value .
As of Swift 5.2, nested property wrappers have become a lot more stable, but they're still a bit difficult to work with. I've written an article here about it, but the trick is that since the outer wrapper's wrappedValue
is the type of the inner wrapper, and the inner wrapper's wrappedValue
is the direct property type, you have to make the wrapper operate on both types.
The basic idea I've followed is to create a protocol which the wrappers operate on. You can then have other wrappers conform to the protocol as well, in order to enable nesting.
For example, in the case of Doubled:
protocol Doublable {
func doubling() -> Self
func halving() -> Self
}
@propertyWrapper
struct Doubled<T: Doublable> {
var number: T
var wrappedValue: T {
get { number.doubling() }
set { number = newValue.halving() }
}
init(wrappedValue: T) {
self.number = wrappedValue
}
}
extension Int: Doublable {
func doubling() -> Int {
return self * 2
}
func halving() -> Int {
return Int(self / 2)
}
}
extension Doubled: Doublable {
func doubling() -> Self {
return Doubled(wrappedValue: self.wrappedValue)
}
func halving() -> Self {
return Doubled(wrappedValue: self.wrappedValue)
}
}
struct Test {
@Doubled @Doubled var value: Int = 10
}
var test = Test()
print(test.value) // prints 40
You could do the same thing for Tripled, with a Tripleable protocol, and so on.
However, I should note that instead of nesting @Tripled @Doubled
, it might be better to create another wrapper like @Multiple(6)
instead: then you won't have to deal with any protocols, but you'll get the same effect.
There were some issues with using multiple property wrappers so that part of the feature was pulled out of Swift 5.1 but it will be available in 5.2. Until then you can’t use multiple property wrappers directly like this.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With