Binding issue

Hi, I'm trying to bind some form data in a sheet screen. The code works fine but I got the warning below in console. Could someone please tell me what I'm missing?
(I'm using prerelease/1.0 branch)

2023-03-07 00:30:35.250828+0900 Sandbox[61762:876505] [ComposableArchitecture] A "ifLet" at "Sandbox/SandboxApp.swift:59" received a presentation action when destination state was absent. …

  Action:
    ContentFeature.Action.subScreenAction(.presented(.binding))

This is generally considered an application logic error, and can happen for a few reasons:

• A parent reducer set destination state to "nil" before this reducer ran. This reducer must run before any other reducer sets destination state to "nil". This ensures that destination reducers can handle their actions while their state is still present.

• This action was sent to the store while destination state was "nil". Make sure that actions for this reducer can only be sent from a view store when state is present, or from effects that start from this reducer. In SwiftUI applications, use a Composable Architecture view modifier like "sheet(store:…)".
2023-03-07 00:30:35.251176+0900 Sandbox[61762:876505] [ComposableArchitecture] A binding action sent from a view store at "Sandbox/SandboxApp.swift:73" was not handled. …

  Action:
    SubContentFeature.Action.binding(.set(_, ))

To fix this, invoke "BindingReducer()" from your feature reducer's "body".
2023-03-07 00:30:35.251393+0900 Sandbox[61762:876505] [ComposableArchitecture] A "ifLet" at "Sandbox/SandboxApp.swift:59" received a presentation action when destination state was absent. …

  Action:
    ContentFeature.Action.subScreenAction(.presented(.binding))

This is generally considered an application logic error, and can happen for a few reasons:

• A parent reducer set destination state to "nil" before this reducer ran. This reducer must run before any other reducer sets destination state to "nil". This ensures that destination reducers can handle their actions while their state is still present.

• This action was sent to the store while destination state was "nil". Make sure that actions for this reducer can only be sent from a view store when state is present, or from effects that start from this reducer. In SwiftUI applications, use a Composable Architecture view modifier like "sheet(store:…)".
2023-03-07 00:30:35.251652+0900 Sandbox[61762:876505] [ComposableArchitecture] A binding action sent from a view store at "Sandbox/SandboxApp.swift:73" was not handled. …

  Action:
    SubContentFeature.Action.binding(.set(_, ))

To fix this, invoke "BindingReducer()" from your feature reducer's "body".

Code

//
//  SandboxApp.swift
//  Sandbox

import ComposableArchitecture
import SwiftUI

@main
struct SandboxApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView(store: .init(initialState: .init(), reducer: ContentFeature()))
        }
    }
}

struct ContentView: View {
    
    var store: StoreOf<ContentFeature>
    
    var body: some View {
        WithViewStore(store, observe: { $0 }) { viewStore in
            VStack {
                Button {
                    viewStore.send(.didTapStartButton)
                } label: {
                    Text("Start")
                }
            }
            .padding()
            .sheet(store: store.scope(state: \.$subScreenState, action: ContentFeature.Action.subScreenAction)) { store in
                SubContentView(store: store)
            }
        }
    }
}

struct ContentFeature: ReducerProtocol {
   
    struct State: Equatable {
        @PresentationState var subScreenState: SubContentFeature.State?
    }
    
    enum Action {
        case didTapStartButton
        case subScreenAction(PresentationAction<SubContentFeature.Action>)
    }
    
    var body: some ReducerProtocol<State, Action> {
        Reduce { state, action in
            switch action {
            case .didTapStartButton:
                state.subScreenState = .init()
                return .none
            default:
                return .none
            }
        }
        .ifLet(\.$subScreenState, action: /Action.subScreenAction) {
            SubContentFeature()
        }
    }
}

struct SubContentView: View {
    
    var store: StoreOf<SubContentFeature>
    
    var body: some View {
        WithViewStore(store, observe: { $0 }) { viewStore in
            NavigationView {
                VStack {
                    TextField("Text", text: viewStore.binding(\.$text))
                        .padding()
                }.toolbar {
                    Button {
                        viewStore.send(.didTapDoneButton)
                    } label: {
                        Text("Done")
                    }
                }.navigationTitle("Sub Screen")
            }
        }
    }
}

struct SubContentFeature: ReducerProtocol {
    
    @Dependency (\.dismiss) var dismiss
    
    struct State: Equatable {
        @BindingState var text = ""
        var dummy = true
    }
    
    enum Action: BindableAction {
        case binding(BindingAction<State>)
        case didTapDoneButton
    }
    
    var body: some ReducerProtocol<State, Action> {
        BindingReducer()
        Reduce { state, action in
            switch action {
            case .didTapDoneButton:
                return .fireAndForget {
                    await dismiss()
                }
            default:
                return .none
            }
        }
    }
}

Hi @waynezhang, there is a known bug in SwiftUI textfields that causes them to write to their bindings way more often than necessary, including when the textfield is removed from the screen. That causes an action to be sent when state has been nil'd out.

Luckily we can work around the issue in the library and it will be fixed soon. For now you can just ignore the warning.

1 Like

@mbrandonw Thanks very much for the reply! Glad we have a workaround. :slight_smile: