4

I'm trying to pass a binding to a variable that is created with a property wrapper. It appears that I lose access to the underlying type, when I pass the binding to another view. In the following example code, I demonstrate that I can update the original bound value, but not when I pass its binding to another view:

import SwiftUI

@propertyWrapper
struct BoundedNumber {
    private var number: Int
    private var minimum: Int
    private var maximum: Int
    
    init(wrappedValue: Int, minimum: Int, maximum: Int) {
        self.minimum = minimum
        self.maximum = maximum
        number = max(minimum, min(wrappedValue, maximum))
    }
    
    var wrappedValue: Int {
        get { return number }
        set { number = max(minimum, min(newValue, maximum)) }
    }
}

struct ContentView: View {
    @State @BoundedNumber(minimum: 0, maximum: 10) var firstNumber: Int = 1
    @State @BoundedNumber(minimum: 1, maximum: 5) var secondNumber: Int = 1
    
    var body: some View {
        VStack {
            HStack {
                Text("\(firstNumber)")
                UpdateButton($firstNumber, updateType: .decrement)
                UpdateButton($firstNumber, updateType: .increment)
            }
            HStack {
                Text("\(secondNumber)")
                UpdateButton($secondNumber, updateType: .decrement)
                UpdateButton($secondNumber, updateType: .increment)
            }
            Button { 
                firstNumber +=  1 // This compiles
            } label: { 
                Image(systemName: "plus")
            }
        }
    }
}

struct UpdateButton: View {
    @Binding var value: BoundedNumber
    let updateType: UpdateType

    init(_ value: Binding<BoundedNumber>, updateType: UpdateType) {
        _value = value
        self.updateType = updateType
    }

    enum UpdateType {
        case increment, decrement
    }

    var body: some View {
        Button { 
            value += updateType == .increment ? 1 : -1  
           // This gives a compiler error: 
           // Binary operator '+=' cannot be applied to operands of type 'BoundedNumber' and '_'
        } label: {
            Image(systemName: updateType == .increment ? "plus" : "minus")
        }
    }
}
Philip Pegden
  • 1,732
  • 1
  • 14
  • 33
  • `value.wrappedValue += updateType == .increment ? 1 : -1` try this, BoundedNumber is a `projectedValue` from @State, and of type `Binding`, what you have is direct access to your wrapper struct, and it’s `wrappedValue` property is what you need. I am not sure if I am completely right here, but why you need it that way a nested wrappers? – Tushar Sharma Mar 06 '21 at 12:34

2 Answers2

1

Add the following extension with operator and your code worked. Tested with Xcode 12.4 / iOS 14.4

extension BoundedNumber {
    static func +=(_ lhs: inout BoundedNumber, _ rhs: Int) {
        lhs.wrappedValue += rhs
    }
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
0

Here is another way to implement a custom property wrapper that depends on another property wrapper (aka nested property wrappers):

import SwiftUI

@propertyWrapper
struct BoundedNumber: DynamicProperty {
    private var number: State<Int> // SwiftUI will update when this changes because the struct is a DynamicProperty
    private var minimum: Int
    private var maximum: Int
    
    init(wrappedValue: Int, minimum: Int, maximum: Int) {
        self.minimum = minimum
        self.maximum = maximum
        number = State<Int>(initialValue: max(minimum, min(wrappedValue, maximum)))
    }
    
    var wrappedValue: Int {
        get { return number.wrappedValue }
        nonmutating set {
            number.wrappedValue = max(minimum, min(newValue, maximum))
        }
    }
    
    var projectedValue: Binding<Int> {
        Binding(get: { wrappedValue }, set: { //_ in
            wrappedValue = $0
        })
    }
}

struct ContentView: View {
    @BoundedNumber(minimum: 0, maximum: 10) var firstNumber = 1
    @BoundedNumber(minimum: 1, maximum: 5) var secondNumber = 1
    
    var body: some View {
        VStack {
            HStack {
                Text("\(firstNumber)")
                UpdateButton($firstNumber, updateType: .decrement)
                UpdateButton($firstNumber, updateType: .increment)
            }
            HStack {
                Text("\(secondNumber)")
                UpdateButton($secondNumber, updateType: .decrement)
                UpdateButton($secondNumber, updateType: .increment)
            }
            Button {
                firstNumber +=  1 // This compiles
            } label: {
                Image(systemName: "plus")
            }
        }
    }
}

enum UpdateType {
    case increment, decrement
}

struct UpdateButton: View {
    @Binding var value: Int
    let updateType: UpdateType
    
    init(_ value: Binding<Int>, updateType: UpdateType) {
        _value = value
        self.updateType = updateType
    }
    
    var body: some View {
        Button {
            value += updateType == .increment ? 1 : -1
            // no more compiler error!
        } label: {
            Image(systemName: updateType == .increment ? "plus" : "minus")
        }
    }
}
malhal
  • 26,330
  • 7
  • 115
  • 133