How to compose swift property wrappers?
Asked Answered
E

3

22

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 and Copying:

@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 type DelayedMutable<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 the NSCopying 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.

Erasion answered 14/1, 2020 at 17:50 Comment(1)
you can look this pod for example cod like this github.com/muhammadali2012/ModelLoki
S
11

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.

Sicard answered 10/5, 2020 at 22:24 Comment(2)
Thank you for this. This is really useful!Erasion
This is the most magical snippet of code I have ever read! So many contexts of meaning. Int can be mismatched with Doubled which is a property wrapper to doubling logic and buffering a result of multiple annotations only to print that doubly doubled ten is equal forty. Nice Noah!End
G
3

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.

Groom answered 14/1, 2020 at 21:38 Comment(3)
Thank you for this. I'll remember to look out for 5.2! Any ideas when it is due?Erasion
5.2 just started to having toolchains available on swift.org so it will probably be a while, I would guess around WWDC. If you aren't deploying this code for a while and are ok with the bugs that come with a development toolchain you could use that to test or develop things with multiple property wrappers.Groom
First beta just dropped! developer.apple.com/documentation/xcode_release_notes/…Jipijapa
L
0

The simplest explanation.

  1. Propery wrappers on a property are applied from left to right and will be inetialised in the same patren.
  2. Type of the porperty wrapper's wrapped value must be same to the type it is applied on.

with that in mind lets think of two wrappers

@propertyWrapper struct TwoTimer {
var wrappedValue : Int
var projectedValue : Int {
    return 2 * wrappedValue
}}

@propertyWrapper struct TenTimer {
var wrappedValue : Int
var projectedValue : Int {
    return 10 * wrappedValue
}}

in simplest form the projected value accesed using $ will return the wrapped value ten times or two times

now lets try to achive

@TenTimer var tenTimevalue = 4
@TwoTimer var twoTimevalue = 4
@TwoTimer @TenTimer var value = 4

the tird line is the one we are most interested in because we want our value to be ten timed and two timed as well but comlier will say that

Cannot convert value of type 'TenTimer' to specified type 'Int'

because the wrappedvalue of TwoTimer the left wrappers (remeber TwoTimer is the left one) is Int, we can simply change it to TenTimer type and it will work ie

@propertyWrapper struct TwoTimer {
var wrappedValue : TenTimer
var projectedValue : Int {
    return 2 * wrappedValue.wrappedValue
}}

this will remove the compiler error but will make TwoTimer to work with only TenTimer types and reslut in very tight coupling and with no reusbility and the purpose of property wrappers dies instantly. To make TwoTimer work with any type we have to use protocols and generics.

lets create a protocol MainType with following recquirements

protocol MainType  {
associatedtype  T
var wrappedValue : T { get set }

}

now lets confimr this type in both of our wrappers

@propertyWrapper struct TenTimer : MainType  {
var wrappedValue : Int
var projectedValue : Int {
    return 10 * wrappedValue
}

}

@propertyWrapper struct TwoTimer : MainType {
var wrappedValue : Int
var projectedValue : Int {
    return 2 * wrappedValue
}

}

now the TwoTimer can work with a MainType instead of TenTimerOnly so let's change that type as well use A generic of MainType in TwoTimer instead of a concrete TenTimer Type to make TwoTimer work with a MainType instead of TenTimerOnly

@propertyWrapper struct TwoTimer<T : MainType> : MainType {
var wrappedValue : T
var projectedValue : Int {
    return 2 * wrappedValue
}

}

almost there but the compiler can't multiply generic type T (which will be the main type ) with Int and raise an error

Cannot convert value of type 'T' to expected argument type 'Int'

and not getting resolved by casting T into Int so

let's create a IntNumericType type with a value function

protocol IntNumericType {
func value() -> Int

}

confirm it in TenTimer

@propertyWrapper struct TenTimer : MainType, IntNumericType  {
var wrappedValue : Int
var projectedValue : Int {
    return 10 * wrappedValue
}
func value() -> Int {
    return wrappedValue
}

}

apply generic where caluse to the Twotimer Generic value and use it in TwoTimer projectid value

@propertyWrapper struct TwoTimer<T : MainType> : MainType where T : IntNumericType {
var wrappedValue : T
var projectedValue : Int {
    return 2 * wrappedValue.value()
}

}

thats it we can usee Twotimer on any value which can confirm to the MainType Protocol.

note :- we can also convert Tentimer into exact implementaion of Towtimer confirm Int with Maintype using extension for more reusability

We can also use apple Numeric protocol for more useabiliy instead of our own IntNumericType.

Example might not seem suitable to use generic at the first place :P

Loki answered 17/2, 2023 at 7:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.