patlown
(patlown)
1
I just started working through the most recent TCA videos going through the Standups app, I have been rebuilding an app I made in Vanilla SwiftUI in TCA as I learn from the series.
I am building a form using text fields and I am unsure if I am using the onChange modifier against the BindingReducer correctly. I've created a minimal example to show what I've come up with.
I have the following questions:
-
Is this the correct usage of the BindingReducer onChange modifier? It seems a little weird to process the logic as an effect when it is quick logic that isn't a side effect.
-
This is just one field, my form will have at least three such fields with additional pre-processing behavior like this, is this the correct approach?
example:
struct OnChangeExampleView: View {
let store: StoreOf<OnChangeExampleFeature>
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
TextField("myField", text: viewStore.$myField)
Text("\(viewStore.myFieldDouble)")
}
}
}
@Reducer
struct OnChangeExampleFeature: Reducer {
struct State: Equatable {
@BindingState var myField = ""
var myFieldDouble: Double
}
enum Action: BindableAction {
case binding(BindingAction<State>)
case setMyFieldDouble(Double)
}
var body: some ReducerOf<Self> {
BindingReducer()
.onChange(of: \.myField) { oldValue, newValue in
Reduce { state, action in
.run { send in
if let value = Double(newValue.filter("0123456789.".contains)) {
await send(.setMyFieldDouble(value))
}
}
}
}
Reduce { state, action in
switch action {
case .binding:
return .none
case let .setMyFieldDouble(val):
state.myFieldDouble = val
return .none
}
}
}
}
#Preview {
OnChangeExampleView(
store: Store(initialState: OnChangeExampleFeature.State(myFieldDouble: 0.0)) {
OnChangeExampleFeature()
._printChanges()
}
)
}
otondin
(Gabriel Tondin)
2
Hey @patlown !
You can also go with like:
var body: some ReducerOf<Self> {
BindingReducer()
Reduce { state, action in
switch action {
case .binding(\.$myField):
guard let value = Double(state.myField.filter("0123456789.".contains))
else { return .none }
return .run { send in
await send(.setMyFieldDouble(value)
}
case .binding:
return .none
case let .setMyFieldDouble(val):
state.myFieldDouble = val
return .none
}
}
}
Generally we advise against this.
Any reason not to mutate the state directly?
BindingReducer()
.onChange(of: \.myField) { oldValue, newValue in
Reduce { state, action in
if let value = Double(newValue.filter("0123456789.".contains)) {
state.myFieldDouble = value
}
return .none
}
}
Per Gabriel's note above, though, you can also switch directly on the binding case of a particular field. Reducer.onChange is more when you want to detect a small change in a larger structure.
Gabriel's version again may work better, but if you are working with a nested structure you can definitely chain onChange(of:) instead.
otondin
(Gabriel Tondin)
4
Also, sometimes you wanna add some debounce, when binding changes, so maybe returning an effect can be helpful:
enum CancelID {
case debounce
}
var body: some ReducerOf<Self> {
BindingReducer()
Reduce { state, action in
switch action {
case .binding(\.$myField):
guard let value = Double(state.myField.filter("0123456789.".contains))
else { return .none }
return .run { send in
await send(.setMyFieldDouble(value)
}
.debounce(id: CancelID.debounce, for: .seconds(1.0), scheduler: mainQueue)
case .binding:
return .none
case let .setMyFieldDouble(val):
state.myFieldDouble = val
return .none
}
}
}
``