I'm looking to implement a generic validation/vetoing loop in SwiftUI - the sort of thing that should be pretty straightforward to do with a "single source of truth" framework
In short I want to:
- Have a generic control (lets say for instance a
TextField
) - Apply a validation/veto on the update of that control (for instance, a user types text)
- Propagate the intended change into a validator, updating a
Binding
source object somewhere (ideally, an@State
member inside theView
) - Feeding that value back into the control for display
It seems that for all the "single source of truth" talk Apple is kind of lying - injecting a validation stage into this chain seems difficult, especially without breaking encapsulation of the view
Note that I don't really want to solve this problem in particular - I'm looking for a pattern to implement (ie: replace the String
and TextField
with Bool
and Toggle
for example)
The following code shows my best attempt at doing the above loop
class ValidatedValue<T>: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
var validator: (T, T)->T
var value: T {
get {
_value
}
set {
_value = validator(_value, newValue)
objectWillChange.send()
}
}
/// Backing value for the observable
var _value: T
init(_ value: T, validator: @escaping (T, T)->T) {
self._value = value
self.validator = validator
}
}
struct MustHaveDTextField: View {
@ObservedObject var editingValue: ValidatedValue<String>
public var body: some View {
return TextField(
"Must have a d",
text: $editingValue.value
}
}
With the validated value defined well outside the scope of the View
ValidatedValue(
"oddity has a d",
validator: { current, new in
if new.contains("d") {
return new
}
else {
return current
}
}
)
This kind of works as it will prevent you from modifying the string input if it contains no "d"s. However;
- The cursor state still moves on the text control past the point of validation
- It exposes what should be entirely internal state and requires passing that down from parents or via the
EnvironmentObject
(if you are doing this withList
s of things...ow)
Either I'm missing something key, or I'm taking the wrong approach, or what Apple says is not what Apple does.
Modifying internal state during the loop like what is done here or here is not good - they modify state inside the view loop which XCode flags as undefined behaviour
. This one also has a similar solution but again suffers from needing to put the validation logic outside the view - IMHO it should be self-contained.