Reputation: 7154
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?
Upvotes: 3
Views: 940
Reputation: 4513
You can't apply multiple propertyWrapper
s, 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
}
}
Upvotes: 2
Reputation: 13012
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!")
})
}
}
}
Upvotes: 3