Nested Property Wrappers in Swift

Published by malhal on

I designed a better way to nest property wrappers than other examples I’ve seen in this article and this Stack Overflow question. The sample below is a redesigned version of the code in the question, which I also posted as an answer. Rather than use multiple property wrappers on the same var, it embeds one inside the other.

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 BindingTest2: 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
        } label: {
            Image(systemName: updateType == .increment ? "plus" : "minus")
        }
    }
}
Categories: SwiftUI