I'm currently trying to modify an upcoming value from a textField which is using a Binding<Double>
, but haven't found any working solution yet. It's only been infinite loops (Like the example below) and other solutions which didn't work in the end anyway. So, for example, if an user inputs an amount which is too low, I would want to change the upcoming value to the minimum and vice verse if the value is higher than the maximum value.
I also want to present the modified
value (if needed) for the user, so I can't just store it in another variable.
Any ideas on how to solve this?
class ViewModel: ObservableObject {
@Published var amount: Double
private var subscriptions: Set<AnyCancellable> = []
private let minimum: Double = 10_000
private let maximum: Double = 100_000
init() {
$amount
.sink {
if $0 < self.minimum {
// Set minimum value
self.amount = self.minimum
} else if $0 > self.maximum {
// Set maximum value
self.amount = self.maximum
}
// If `Else` is implemented it will just be an infinite loop...
else {
self.amount = $0
}
}
.store(in: &subscriptions)
}
func prepareStuff() {
// Start preparing
let chosenAmount = amount
}
}
One way is to use a property wrapper to clamp the values.
Here is a very basic example of the issue, where we have an amount
, that we can change to any value. The Stepper
just makes it easy for input/testing:
struct ContentView: View {
@State private var amount = 0
var body: some View {
Form {
Stepper("Amount", value: $amount)
Text(String(amount))
}
}
}
The problem with this example is that amount
isn't limited to a range. To fix this, create a Clamping
property wrapper (partially from here):
@propertyWrapper
struct Clamping<Value: Comparable> {
private var value: Value
let range: ClosedRange<Value>
var wrappedValue: Value {
get { value }
set { value = min(max(range.lowerBound, newValue), range.upperBound) }
}
var clampedValue: Value {
get { wrappedValue }
set { wrappedValue = newValue }
}
init(wrappedValue value: Value, _ range: ClosedRange<Value>) {
precondition(range.contains(value))
self.value = value
self.range = range
}
}
And then we can chain property wrappers, and get a working example where amount
is limited:
struct ContentView: View {
@State @Clamping(-5 ... 5) private var amount = 0
var body: some View {
Form {
Stepper("Amount", value: $amount.clampedValue)
Text(String(amount))
}
}
}
I know, this isn't the proper way to limit a Stepper
's range. Instead you should use Stepper(_:value:in:)
. However, this is to instead demonstrate clamping a value - not how to clamp a Stepper
.
Well, first off change your @Published
property to this:
@Published @Clamping(10_000 ... 100_000) var amount: Double
And now you can just access amount
like normal to get the clamped value. Use $amount.clampedValue
like I did in my solution to get your Binding<Double>
binding.
If having troubles sometimes with compiling chained property wrappers (probably a bug), here is my example recreated using a Model
object and @Published
:
struct ContentView: View {
@StateObject private var model = Model(amount: 0)
var body: some View {
Form {
Stepper("Amount", value: $model.amount.clampedValue)
Text(String(model.amount.clampedValue))
}
}
}
class Model: ObservableObject {
@Published var amount: Clamping<Int>
init(amount: Int) {
_amount = Published(wrappedValue: Clamping(wrappedValue: amount, -5 ... 5))
}
}