0

Consider the following property wrapper. It keeps track of value in wrappedValue, and also caches old value in wrappedValueOld.

@propertyWrapper struct OldValueCache<Value>: DynamicProperty {
    @State private var field: Value
    var wrappedValue: Value {
        get {
            field
        }
        nonmutating set {
            wrappedValueOld = field
            field = newValue
        }
    }

    @State private(set) var wrappedValueOld: Value?

    init(wrappedValue: Value) {
        self._field = .init(initialValue: wrappedValue)
    }
}

To test it, consider the following View:

struct ContentView: View {
    @OldValueCache var a: Int = 0
    
    var body: some View {
        VStack(content: {
            Text("\(a) \(_a.wrappedValueOld.map { .init($0) } ?? "nil")")
            
            Button("Increment", action: { a += 1 })
        })
            .onAppear(perform: foo)
    }
    
    private func foo() {
        @OldValueCache var b: Int = 0
        print(b, _b.wrappedValueOld.map { .init($0) } ?? "nil")
        
        b += 1
        print(b, _b.wrappedValueOld.map { .init($0) } ?? "nil")
    }
}

It contains a Text that displays current and old value, and a Button that increments that value. By tapping on the button, we can see a proper behavior. Text shows "0 nil", then "1 0", "2 1", "3 2", etc.

However, variable with that property wrapper breaks in a method. foo is called on onAppear(perform:) which should print "0 nil" and then "1 0". But, "0 nil" is printed each time. DynamicProperty protocol seems to cause this.

Alternately, we can remove conformance and end up with:

@propertyWrapper struct OldValueCache<Value> {
    private var field: Value
    var wrappedValue: Value {
        get {
            field
        }
        set {
            wrappedValueOld = field
            field = newValue
        }
    }

    private(set) var wrappedValueOld: Value?

    init(wrappedValue: Value) {
        self.field = wrappedValue
    }
}

This time, foo method works properly, but since OldValueCache is no longer dynamic, body breaks, and Button needs to be commented out.

Is this expected behavior? What are the possible solutions to make a property wrapper work in both cases, without duplicating the declaration?

Vakho
  • 87
  • 7

0 Answers0