Getting Runtime warning on setting optional state nil only if the view is wrapped inside navigation #1901
-
Hi guys! So, recently I've noticed this kind of runtime warning Warning log2023-02-11 14:26:52.386376+0400 AVRecorder[68312:16891785] [ComposableArchitecture] An "ifLet" at "AVRecorder/AppCoordinatorReducer.swift:78" received a child action when child state was "nil". …Action: This is generally considered an application logic error, and can happen for a few reasons: • A parent reducer set child state to "nil" before this reducer ran. This reducer must run before any other reducer sets child state to "nil". This ensures that child reducers can handle their actions while their state is still available. • An in-flight effect emitted this action when child state was "nil". While it may be perfectly reasonable to ignore this action, consider canceling the associated effect before child state becomes "nil", especially if it is a long-living effect. • This action was sent to the store while state was "nil". Make sure that actions for this reducer can only be sent from a view store when state is non-"nil". In SwiftUI applications, use "IfLetStore". Action: This is generally considered an application logic error, and can happen for a few reasons: • A parent reducer set child state to "nil" before this reducer ran. This reducer must run before any other reducer sets child state to "nil". This ensures that child reducers can handle their actions while their state is still available. • An in-flight effect emitted this action when child state was "nil". While it may be perfectly reasonable to ignore this action, consider canceling the associated effect before child state becomes "nil", especially if it is a long-living effect. • This action was sent to the store while state was "nil". Make sure that actions for this reducer can only be sent from a view store when state is non-"nil". In SwiftUI applications, use "IfLetStore". When does it happen?It happens only if the What I want to achieveI want to show either login screen or the root view. After user logs in, login screen should be removed completely and the root view should be shown. How I try to do it
How to reproduceLet's say struct FeatureA: ReducerProtocol {
struct State: Equatable {
var featureB: FeatureB.State?
var featureC: FeatureC.State?
}
enum Action {
case featureB(FeatureB.Action)
case featureC(FeatureC.Action)
case requestInitialScreen
}
var body: some ReducerProtocol<State, Action> {
Reduce { state, action in
switch action {
case .requestInitialScreen:
state.featureB = .init(text: "", isButtonDisabled: true)
return .none
case let .featureB(.taskCompleted(result)):
print("Feature B completed with result:", result)
state.featureB = nil
state.featureC = .init()
return .none
default:
return .none
}
}
.ifLet(\.featureB, action: /Action.featureB) {
FeatureB()
}
.ifLet(\.featureC, action: /Action.featureC) {
FeatureC()
}
}
} struct FeatureB: ReducerProtocol {
struct State: Equatable {
var text: String
var isButtonDisabled: Bool
var isLoading: Bool = false
}
enum Action {
case performTask
case taskCompleted(String)
case updateText(String)
}
var body: some ReducerProtocol<State, Action> {
Reduce { state, action in
switch action {
case .performTask:
state.isLoading = true
return .task { [result = state.text] in
try await Task.sleep(for: .seconds(3))
return .taskCompleted(result)
}
case let .updateText(text):
state.text = text
state.isButtonDisabled = text.isEmpty
return .none
case .taskCompleted:
state.isLoading = false
return .none
}
}
}
} struct FeatureC: ReducerProtocol {
struct State: Equatable {
// ...
}
enum Action {
// ...
}
var body: some ReducerProtocol<State, Action> {
Reduce { state, action in
return .none
}
}
} struct FeatureAView: View {
private typealias Action = FeatureA.Action
let store: StoreOf<FeatureA>
var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
IfLetStore(
store.scope(state: \.featureB, action: Action.featureB),
then: FeatureBView.init,
else: {
IfLetStore(
store.scope(state: \.featureC, action: Action.featureC),
then: FeatureCView.init
)
}
)
.task {
viewStore.send(.requestInitialScreen)
}
}
}
}
struct FeatureBView: View {
private typealias Action = FeatureB.Action
let store: StoreOf<FeatureB>
var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
NavigationStack { // If you comment out NavigationStack issue doesn't exist
VStack {
TextField(
"",
text: viewStore.binding(
get: \.text,
send: Action.updateText
)
)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("title from view state") {
viewStore.send(.performTask)
}
.disabled(viewStore.isButtonDisabled)
}
}
}
}
}
struct FeatureCView: View {
let store: StoreOf<FeatureC>
var body: some View {
Text("Feature C")
}
} As I see textfield binding gets called after the optional state in Notes
I believe I'm missing something simple or maybe even doing something stupid so I'd better ask for your help 😃 |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 10 replies
-
Hey @kakhaberikiknadze! I didn't run your code, so I can't explain yet why it doesn't trigger the warning when you remove the .task {
ViewStore(store.stateless).send(.requestInitialScreen)
} You can probably safely ignore this warning for now. |
Beta Was this translation helpful? Give feedback.
-
Hello, I've encountered the same situation and I'm not sure how to resolve it. |
Beta Was this translation helpful? Give feedback.
Hey @kakhaberikiknadze!
TextField
is indeed a very vocal view that frequently over-emits on its binding. For example, it sets the the value when it loses focus, and this is very likely what you're observing whenFeatureB
is dismissed:FeatureB.State
isnil
'd, the view goes away, and if theTextField
had focus, it emits on the binding when losing it, which triggers the warning becauseFeatureB.State
is nil. There are experiments to disable this warning when the action is aBindingAction
, but it would only work when using theBindingReducer
. It is also possible that this warning can also be bypassed if the upcoming navigation utilities are used.I didn't run your code, so I can't explain ye…