To fix this, invoke BindingReducer() from your feature reducer's "body" -- but it is

Love exploring this package but I'm having some trouble understanding a debug statement. I have a Form of Sections with TextFields, each of which is connected to @BindingState variables in my feature's State. I have a binding action, my Action implements BindableAction, and I have a top level BindingReducer initialized in my feature's reducer. Yet when I try to type in any of the TextFields, I get one or two letters, then the contents erase and reset, and I get a purple error (recreated below). I have compared my code to elsewhere in my project where I have a similar set up (which works) and simply cannot identify where I've gone astray. Further, I'm curious if I'm missing something conceptually about how to debug these type of issues. I know TextField has some errors with bindings but I'm not sure if this is related.

import ComposableArchitecture
import SwiftUI

struct NewDeck: Reducer {
    @Dependency(\.uuid) var uuid
    @Dependency(\.date.now) var date
    
    public struct State: Equatable {
        @BindingState var focus: Field? = .name
        @BindingState var name: String = "" // these are the faulty BindingStates
        @BindingState var subject: String = ""
        @BindingState var tagsString: String = ""
        @BindingState var selectedColor: Color = .white
        
        init(focus: Field = .name) {
            self.focus = focus
        }
        
        enum Field: Hashable {
            case name
            case subject
            case tags
        }
    }
    
    public enum Action: BindableAction, Equatable {
        case binding(BindingAction<State>)
        case delegate(Delegate)
        case saveDeck
        
        enum Delegate: Equatable {
            case saveDeck(DeckModel)
        }
    }
    
    var body: some ReducerOf<Self> {
        BindingReducer()
        Reduce { state, action in
            switch action {
            case .binding:
                return .none
                
            case .delegate:
                return .none
                
            case .saveDeck:
                let deck = DeckModel(id: uuid(), name: state.name, dateCreated: date, dateLastStudied: date)
                return .run { send in
                    await send(.delegate(.saveDeck(deck)))
                }
            }
        }
    }
}

struct NewDeckView: View {
    var store: StoreOf<NewDeck>
    @FocusState var focus: NewDeck.State.Field?
    
    var body: some View {
        WithViewStore(self.store, observe: { $0 }) { viewStore in
            Form {
                Section(header: Text("Deck Information")) {
                    TextField(
                        "Name",
                        text: viewStore.$name
                    )
                    .textInputAutocapitalization(.never)
                    .autocorrectionDisabled()
                    .focused(self.$focus, equals: .name)
                    
                    TextField(
                        "Subject",
                        text: viewStore.$subject
                    )
                    .textInputAutocapitalization(.never)
                    .autocorrectionDisabled()
                    .focused(self.$focus, equals: .subject)
                    
                    TextField(
                        "Tags (separated by spaces)",
                        text: viewStore.$tagsString
                    )
                    .textInputAutocapitalization(.never)
                    .autocorrectionDisabled()
                    .focused(self.$focus, equals: .tags)
                }
                
                Section(header: Text("Color")) {
                    ColorPicker("Color", selection: viewStore.$selectedColor, supportsOpacity: false)
                }
                
                Button {
                    viewStore.send(.saveDeck)
                } label: {
                    Text("Save Deck")
                }
            }
            .navigationBarTitle("Create New Deck", displayMode: .inline)
        }
    }
}

### Error Message
2023-08-15 07:38:04.306789-0400 arabic-pencil-app[40994:4234536] [ComposableArchitecture] A binding action sent from a view store for binding state defined at "arabic_pencil_app/NewDeck.swift:23" was not handled. …

  Action:
    NewDeck.Action.binding(.set(_, ""))

To fix this, invoke "BindingReducer()" from your feature reducer's "body".
2023-08-15 07:38:04.307136-0400 arabic-pencil-app[40994:4234536] [ComposableArchitecture] A binding action sent from a view store for binding state defined at "arabic_pencil_app/NewDeck.swift:23" was not handled. …

  Action:
    NewDeck.Action.binding(.set(_, ""))

To fix this, invoke "BindingReducer()" from your feature reducer's "body".
[ad infinitum, multiple per keystroke]

Hoping some sharp eyed forum citizen can guide me down from the mountains of madness over this. Thanks!

Hi @ocapmycap, I just ran your code in a fresh project and it seemed to work just fine. No warnings. And everything in the code looks good as far as I can tell. Can you provide a minimal reproducing project that demonstrates the problem?

@mbrandonw I'm always impressed by how quickly you reply!

I would still be interested in getting help (or learning) what was going wrong, but I found the bit of code that, when removed, cleared up the errors. I had a Library that held a DeckList and included a plus button that would present the above AddDeck view.

The DeckList feature had an enum of a single Delegate case from a previous structure. The delegate was not used in any way, its Reducer action was return .none. But removing that cruft made everything work. I will reproduce a minimal version of this when I'm on my personal machine. Thanks so much.

Might have jumped the gun on proclaiming success. Below is a minimal reproduction that has the same behavior (TextField will not fill out) and emits the same error about binding state.

I'm sure I have a conceptual error somewhere, if you see it, thanks in advance!

import ComposableArchitecture
import SwiftUI

@main
struct MTVApp: App {
    var body: some Scene {
        WindowGroup {
            DeckListView(store: Store(initialState: DeckList.State(decks: IdentifiedArray(uniqueElements: [.preview])), reducer: { DeckList() }))
        }
    }
}

struct DeckList: Reducer {
    
    struct State: Equatable {
        var decks: IdentifiedArrayOf<DeckModel>
        @PresentationState var destination: Destination.State? = nil
    }
    
    enum Action: Equatable {
        case createDeckButtonTapped
        case destination(PresentationAction<Destination.Action>)
        case disappeared
        case dismissNewDeckButtonTapped
        case saveDeckButtonTapped
    }
    
    struct Destination: Reducer {
        enum State: Equatable {
            case add(NewDeck.State)
        }
        enum Action: Equatable, Sendable {
            case add(NewDeck.Action)
        }
        
        var body: some ReducerOf<Self> {
            Scope(state: /State.add, action: /Action.add) {
                NewDeck()
            }
        }
    }
    
    var body: some ReducerOf<Self> {
        Reduce<State, Action> { state, action in
            switch action {
            case .createDeckButtonTapped:
                state.destination = .add(
                    NewDeck.State(
                        focus: .name,
                        deck: DeckModel(
                            name: ""
                        )
                    )
                )
                return .none
            case .destination:
                return .none
            case .dismissNewDeckButtonTapped:
                state.destination = nil
                return .none
            case .disappeared:
                state.destination = nil
                return .none
            case .saveDeckButtonTapped:
                state.destination = nil
                return .none
            }
        }
    }
}

struct DeckListView: View {
    let store: StoreOf<DeckList>
    
    var body: some View {
        WithViewStore(self.store, observe: { $0 }) { viewStore in
            NavigationStack {
                VStack {
                    List {
                      Text("Thing")
                    }
                    .toolbar {
                        Button {
                            viewStore.send(.createDeckButtonTapped, animation: .default)
                        } label: {
                            Image(systemName: "plus")
                        }
                    }
                    .navigationTitle("Decks")
                    .sheet(
                        store: self.store.scope(state: \.$destination, action:  { .destination($0) }),
                        state: /DeckList.Destination.State.add,
                        action: DeckList.Destination.Action.add
                    ) { store in
                        NavigationStack {
                            NewDeckView(store: store)
                                .onDisappear {
                                    viewStore.send(.disappeared)
                                }
                                .navigationTitle("New deck")
                                .toolbar {
                                    ToolbarItem(placement: .cancellationAction) {
                                        Button("Dismiss") {
                                            viewStore.send(.dismissNewDeckButtonTapped)
                                        }
                                    }
                                    ToolbarItem(placement: .confirmationAction) {
                                        Button("Save deck") {
                                            viewStore.send(.saveDeckButtonTapped)
                                        }
                                    }
                                }
                        }
                    }
                }
            }

        }
    }
}

struct DeckList_Previews: PreviewProvider {
    static var previews: some View {
        DeckListView(
            store: Store(
                initialState: DeckList.State(
                    decks: IdentifiedArray(uniqueElements: [.preview])
                ),
                reducer: { DeckList() }
            )
        )
    }
}
struct DeckModel: Equatable, Identifiable, Hashable {
    var id: Self { self }
    var name: String
    
    static let preview: DeckModel = DeckModel(name: "Unique")
}
struct NewDeck: Reducer {
    @Dependency(\.uuid) var uuid
    @Dependency(\.date.now) var now
    
    public struct State: Equatable {
        @BindingState var focus: Field? = .name
        @BindingState var deck: DeckModel
        
        init(focus: Field = .name, deck: DeckModel) {
            self.focus = focus
            self.deck = deck
        }
        
        enum Field: Hashable {
            case name
        }
    }
    
    public enum Action: BindableAction, Equatable {
        case binding(BindingAction<State>)
    }
    
    var body: some ReducerOf<Self> {
        BindingReducer()
        Reduce { state, action in
            switch action {
            case .binding:
                return .none
            }
        }
    }

}

struct NewDeckView: View {
    var store: StoreOf<NewDeck>
    @FocusState var focus: NewDeck.State.Field?
    
    var body: some View {
        WithViewStore(self.store, observe: { $0 }) { viewStore in
            Form {
                Section {
                    TextField("Name", text: viewStore.$deck.name)
                        .textInputAutocapitalization(.never)
                        .autocorrectionDisabled()
                        .focused(self.$focus, equals: .name)
                } header: {
                    Text("DeckInformation")
                }
            }
            .navigationBarTitle("Create New Deck", displayMode: .inline)
        }
    }
}

struct NewDeckView_Previews: PreviewProvider {
    static var previews: some View {
        NewDeckView(store: Store(initialState: NewDeck.State(deck: .preview), reducer: { NewDeck() }))
    }
}

Hi @ocapmycap, you are missing the integration of the Destination reducer with the parent DeckList reducer:

struct DeckList: Reducer {
  …

  var body: some ReducerOf<Self> {
    Reduce<State, Action> { state, action in
      …
    }
    .ifLet(\.$destination, action: /Action.destination) {
      Destination()
    }
  }
}

I guess it is strange to get a warning about the binding in this case, but it technically is correct. Since the reducers weren't integrated together, the binding action had no affect on the state whatsoever.

Hey! I've just "copy&paste" your code above and it has worked for me...

Another thing is, bear in mind you'd like to be able to listen to child feature actions, so you need to add it in your parent feature:

.ifLet(\.$destination, action: /Action.destination) {
    Destination()
}
1 Like

@mbrandonw and @otondin Thank you both so much for your help. This was exactly what I needed!

1 Like