Replies: 6 comments 1 reply
-
Hi @ldstreet! @mbrandonw and I discussed this exact behavior a few weeks ago. While it's definitely a gotcha, we'd probably suggest the same workarounds right now if this is the behavior you want. We're definitely open to discuss improvements here, though! Alternatively, you could potentially validate the text field in other ways if you're up for a different user experience:
|
Beta Was this translation helpful? Give feedback.
-
I think the simplest thing you could do to ensure the state changes when the user exceeds the truncation limit is to track the typed text separately from the truncated text in two separate properties. You could also use this to drive the appearance of a useful error message. |
Beta Was this translation helpful? Give feedback.
-
What is working for me is to have two separate actions - one for the binding and one for the sanitisation: struct ComponentEnvironment {
var mainQueue: AnySchedulerOf<DispatchQueue>
let sanitizeNameInput: (String) -> Effect<String, Never>
}
// ..
ComponentEnvironment(
mainQueue: DispatchQueue.main.eraseToAnyScheduler(),
sanitizeNameInput: { Effect(value: String($0.prefix(5))) }
)
// ..
let componentReducer = Reducer<Component, ComponentAction, ComponentEnvironment> { state, action, environment in
switch action {
case .nameDidChange(let name):
state.ingredient.name = name
return environment
.sanitizeNameInput(name)
.receive(on: environment.mainQueue)
.map(ComponentAction.nameSanitizationResponse)
.eraseToEffect()
case .nameSanitizationResponse(let sanitized):
state.ingredient.name = sanitized
// ...
}
} ✌️ |
Beta Was this translation helpful? Give feedback.
-
I'm going to convert this into a discussion since we don't currently consider it a bug, but we are down to improve things with feedback. |
Beta Was this translation helpful? Give feedback.
-
Just checking back in here to say I've just run into this issue myself - unfortunately the workaround I suggested myself doesn't work. Even if I separately track the pre-validated input in a separate state property (and I can see a state change using reducer.debug() in the console) the text field still does not update to reflect the validated/truncated text. For example, if the text field currently shows "123.45" and our state: struct State {
@BindableState var input: String = "123.45"
var parsedInput: Double = 123.45
var rawInput: String = "123.45"
}
// state.input = "123.45"
// state.parsedInput = 123.45
// state.rawInput = "123.45" Assuming the logic is to just cap fractional digits to 2, then on typing an additional "1" results in this: // state.input = "123.45" (unchanged)
// state.parsedInput = 123.45 (unchanged)
// state.rawInput = "123.451" The The only workaround that works for me is the one @myurieff suggested above, which is performing the validation/sanitisation after a small queue hop - this works although you do briefly see the invalid value before it changes back again. |
Beta Was this translation helpful? Give feedback.
-
@lukeredpath Would you mind seeing if one of these two works? iampatbrown/FixedTextField.swift It's been a while since I've used them but |
Beta Was this translation helpful? Give feedback.
-
Description:
When binding a TextField to a piece of Equatable state and layering on logic in the reducer that causes the the state to be unchanged (i.e. adding character limits), TextField diverges from its binding and renders the typed text even though it doesn't match the value held in state. This seems to be due to the view store filtering out the state change since the logic doesn't trigger an Equatable difference.
This might actually be a SwiftUI bug rather than a ComposableArchitecture bug since it seems to break the contract of a binding to have any other value but that bindings value render on screen.
Example:
Expected Outcome:
The TextField's text value should remain in sync with the value held in state.
Workaround:
For now I can work around this by handling the removeDuplicates closure on WithViewStore explicitly rather than relying on the Equatable conformance...but this definitely seems suboptimal.
I could also add a piece of state that I modify simply to signal that the text has been processed via the reducer (toggle a boolean? set a UUID?) and thus re-render the UI. This is also less than ideal.
Environment:
Beta Was this translation helpful? Give feedback.
All reactions