Error requiring call to BindingReducer() when no ReducerProtocol is used

I have a fairly simple project that is used for managing stored locations. The main screen allows you to pick a location for navigation or editing, and a second screen appears to edit the details of that locations. The edit screen is where I'm having a problem.

I've implemented everything mentioned above, but having some issues with the form for editing location details. When I type text into the fields, they remain empty, and I've noticed an error:
To fix this, invoke "BindingReducer()" from your feature reducer's "body".

I'm not sure how I'm meant to do this though. The reducer powering the edit screen is defined as
let editReducer = AnyReducer<EditState, EditAction, EditEnvironment> and when I try and use BindingReducer() there I get:
Cannot convert value of type 'BindingReducer<State, Action>' to expected argument type 'AnyReducer<EditState, EditAction, EditEnvironment>'

I did some searching and the only references I can find relate to ReducerProtocol , which I'm not currently using (which could be part of the problem).

Can anyone provide some guidance?

Ha, I can see how this is confusing since the error messaging was updated to the non-deprecated APIs. In the deprecated API, you can invoke .binding() on your reducer, instead:

let editReducer = AnyReducer { state, action, environment in
  // ...
}
.binding()  // ⬅️

Hey, thanks for the reply. I think there is actually something more to this. I double checked and I have .binding() on both reducers as well as the actions conforming to BindableAction. I've tried to distill this down to the simplest version possible, and I'm still getting the issue with this as the edit screen reducer:

public struct EditState: Equatable, Hashable {
    @BindableState var name: String = ""
}

public enum EditAction: BindableAction {
    case binding(BindingAction<EditState>)
}

public struct EditEnvironment {
    public init() {}
}

let editReducer = AnyReducer<EditState, EditAction, EditEnvironment>.combine(
    .init { state, action, environment in
        switch action {
        case .binding:
            return .none
        }
    }
)
.binding()

Which is loaded from the main screen:

public enum TileRoute: Hashable {
    case edit(EditState)
}

public struct TilesState: Equatable, Hashable {

    var navigation: [TileRoute] = []

    var editState: EditState? {
        get { navigation.find(/TileRoute.edit) }
        set { newValue.map { navigation.update(/TileRoute.edit, with: $0) } }
    }

    public init() {}
}

public enum TilesAction: BindableAction {
    case navigationPathChanged([TileRoute])
    case editScreen(EditAction)
    case addLocation
    case binding(BindingAction<TilesState>)
}

public struct TilesEnvironment {
    public init() {}
}

public let tilesReducer = AnyReducer<TilesState, TilesAction, TilesEnvironment>.combine(
    .init { state, action, environment in
        switch action {

        case .navigationPathChanged(let navigation):
            state.navigation = navigation
            return .none

        case .editScreen:
            return .none

        case .addLocation:
            let route = TileRoute.edit(EditState())
            state.navigation.append(route)
            return .none

        case .binding:
            return .none
        }
    }
).binding()

I'm also using the this extension for navigation with iOS 16 NavigationStack:

extension Array where Element == TileRoute {
    func find<Value>(_ casePath: CasePath<Element, Value>) -> Value? {
        compactMap { element in
            guard let value = casePath.extract(from: element) else { return nil }
            return value
        }.first
    }
    mutating func update<Value>(_ casePath: CasePath<Element, Value>, with value: Value)
    where Value: Equatable {
        guard
            let routeIndex = firstIndex(where: { element in
                guard casePath.extract(from: element) != .none else { return false }
                return true
            })
        else {
            return
        }
        self[routeIndex] = casePath.embed(value)
    }
}

Where there is a ToolbarItemGroup with a Button that triggers the .addLocation action and .navigationDestination defined as:

.navigationDestination(for: TileRoute.self, destination: { route in
    switch route {
    case .edit:
        IfLetStore(store.scope(state: \.editState, action: TilesAction.editScreen)) { store in
            EditScreen(store: store)
        }
    }
})

Hey @raydowe,

In your example tileReducer does not include your editReducer. You would need to pull it back eg.

public let tilesReducer = AnyReducer<TilesState, TilesAction, TilesEnvironment>.combine(
    editReducer
        .optional()
        .pullback(
            state: \TilesState.editState,
            action: /TilesAction.editScreen,
            environment: { _ in .init() }
        ),
    .init { state, action, environment in
        ...
    }
).binding()

Also, the binding modifier on tileReducer might not be necessary as TileState is not using @BindableState. Not sure if it's just in the example though.

Yes, that's what I was missing. Thank you!

1 Like