3

I have a @State variable that I that I want to add a certain constraint to, like this simplified example:

@State private var positiveInt = 0 {
    didSet {
        if positiveInt < 0 {
            positiveInt = 0
        }
    }
}

However this doesn't look so nice (it seems to be working though) but what I really want to do is to subclass or extend the property wrapper @State somehow so I can add this constraint in it's setter. But I don't know how to do that. Is it even possible?

6
  • 1
    State is a struct, so you cannot subclass it, also, you cannot apply multiple property wrappers, as far as I know. Writing your own may work, but I have no idea what API you need to interact with SwiftUI-s update loop. Sorry for bad news only. Commented Oct 10, 2019 at 11:39
  • @gujci "to interact with SwiftUI-s update loop" - could it be so simple as to call the body property? Or is it like to calling drawRect: in UIKit maybe, it a big NO? Commented Oct 10, 2019 at 11:49
  • I have no documentation and haven't tried getting self.body, but I think it's kinda forbidden Commented Oct 10, 2019 at 12:32
  • I think you should tweak your question a little bit? Because obvious solution for your positiveInt problem would be using Uint instead of Int. Commented Oct 10, 2019 at 13:30
  • @turingtested slightly improved my answer. Commented Oct 10, 2019 at 16:00

2 Answers 2

3

You can't subclass @State since @State is a Struct. You are trying to manipulate your model, so you shouldn't put this logic in your view. You should at least rely on your view model this way:

class ContentViewModel: ObservableObject {
    @Published var positiveInt = 0 {
        didSet {
            if positiveInt < 0 {
                positiveInt = 0
            }
        }
    }
}

struct ContentView: View {
    @ObservedObject var contentViewModel = ContentViewModel()

    var body: some View {
        VStack {
            Text("\(contentViewModel.positiveInt)")
            Button(action: {
                self.contentViewModel.positiveInt = -98
            }, label: {
                Text("TAP ME!")
            })
        }
    }
}

But since SwiftuUI is not an event-driven framework (it's all about data, model, binding and so forth) we should get used not to react to events, but instead design our view to be "always consistent with the model". In your example and in my answer here above we are reacting to the integer changing overriding its value and forcing the view to be created again. A better solution might be something like:

class ContentViewModel: ObservableObject {
    @Published var number = 0
}

struct ContentView: View {
    @ObservedObject var contentViewModel = ContentViewModel()

    private var positiveInt: Int {
        contentViewModel.number < 0 ? 0 : contentViewModel.number
    }

    var body: some View {
        VStack {
            Text("\(positiveInt)")
            Button(action: {
                self.contentViewModel.number = -98
            }, label: {
                Text("TAP ME!")
            })
        }
    }
}

Or even simpler (since basically there's no more logic):

struct ContentView: View {
    @State private var number = 0

    private var positiveInt: Int {
        number < 0 ? 0 : number
    }

    var body: some View {
        VStack {
            Text("\(positiveInt)")
            Button(action: {
                self.number = -98
            }, label: {
                Text("TAP ME!")
            })
        }
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

Actually I want to keep the constraint to the view, since it's a control that only can show a certain range of values. So the last suggestion with a computed property suits best.
2

You can't apply multiple propertyWrappers, but you can use 2 separate wrapped values. Start with creating one that clamps values to a Range:

@propertyWrapper
struct Clamping<Value: Comparable> {
    var value: Value
    let range: ClosedRange<Value>

    init(wrappedValue value: Value, _ range: ClosedRange<Value>) {
        precondition(range.contains(value))
        self.value = value
        self.range = range
    }

    var wrappedValue: Value {
        get { value }
        set { value = min(max(range.lowerBound, newValue), range.upperBound) }
    }
}

Next, create an ObservableObject as your backing store:

class Model: ObservableObject {

    @Published
    var positiveValue: Int = 0

    @Clamping(0...(.max))
    var clampedValue: Int = 0 {
        didSet { positiveValue = clampedValue }
    }
}

Now you can use this in your content view:

    @ObservedObject var model: Model = .init()

    var body: some View {
        Text("\(self.model.positiveValue)")
            .padding()
            .onTapGesture {
                 self.model.clampedValue += 1
            }
    }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.