Why am I not receiving an Action after entering text in the textField? #3266
-
I was practicing with an app similar to the Search app based on the TCA Case example. After entering text in the text field and navigating to the detail screen, the API communication for the detail screen's Store starts and succeeds, but I do not receive a response from the Action. Additionally, if I do not receive the Action response and then close and re-enter the screen, I receive the Action response. However, if I enter text in the text field again and navigate to the detail screen, I do not receive the Action response once more. Upon logging, the data from the API function is decoded and the Model is generated. What could be the issue? Simulator.Screen.Recording.-.iPhone.15.Pro.-.2024-08-01.at.07.47.06.mp4SearchFeature@Reducer
struct SearchFeature {
@ObservableState
struct State: Equatable {
@Presents var alertState: AlertState<Action.Alert>?
var isLoading: Bool = false
var cityList: [CitySearchModel.Result] = []
var searchQuery = ""
}
enum Action {
case alertAction(PresentationAction<Alert>)
case search
case searchQueryChange(String)
case searchResponse(Result<CitySearchModel, Error>)
enum Alert: Equatable { }
}
@Dependency(\.weatherClient) var weatherClient
private enum CancelID {
case request
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .alertAction:
return .none
case .search:
guard state.searchQuery.isEmpty == false else {
state.alertState = AlertState { TextState("Please enter") }
return .none
}
state.isLoading = true
return .concatenate(
.cancel(id: CancelID.request),
.run { [state] send in
await send(
.searchResponse(
Result {
try await self.weatherClient.citySearch(query: state.searchQuery)
}
)
)
}.cancellable(id: CancelID.request, cancelInFlight: true)
)
case .searchQueryChange(let query):
state.searchQuery = query
return .none
case .searchResponse(.failure(let error)):
state.isLoading = false
state.cityList = []
state.alertState = AlertState { TextState("\(error.localizedDescription)") }
return .none
case .searchResponse(.success(let model)):
state.isLoading = false
state.cityList = model.results
return .none
}
}
.ifLet(\.alertState, action: \.alertAction)
}
} SearchViewstruct SearchView: View {
@Bindable var store: StoreOf<SearchFeature>
@FocusState private var isFocused: Bool
var body: some View {
NavigationStack {
ZStack {
VStack(alignment: .leading) {
HStack {
Image(systemName: "magnifyingglass")
TextField(
"City Name...",
text: $store.searchQuery.sending(\.searchQueryChange)
)
.focused($isFocused)
.textFieldStyle(.roundedBorder)
.autocapitalization(.none)
.disableAutocorrection(true)
Button("Search") {
store.send(.search)
}
}
.padding()
List {
ForEach(store.cityList) { model in
NavigationLink(
destination: WeatherDetailView(
store: StoreOf<WeatherDetailFeature>(
initialState: WeatherDetailFeature.State(cityModel: model),
reducer: {
WeatherDetailFeature()
._printChanges()
}
)
)
) {
Text(model.name)
}
}
}
}
if store.isLoading {
ProgressView("Searching...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black.opacity(0.3))
.edgesIgnoringSafeArea(.all)
.foregroundColor(.white)
}
}
.navigationTitle("City Search")
}
.alert($store.scope(state: \.alertState, action: \.alertAction))
}
} WeatherDetailFeature@Reducer
struct WeatherDetailFeature {
@ObservableState
struct State: Equatable {
@Presents var alertState: AlertState<Action.Alert>?
let cityModel: CitySearchModel.Result
var weatherModel: WeatherModel?
var isLoading = true
var dismiss = false
}
enum Action {
case alertAction(PresentationAction<Alert>)
case getWeatherData
case weatherResponse(Result<WeatherModel, Error>)
enum Alert: Equatable { }
}
@Dependency(\.weatherClient) var weatherClient
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .alertAction:
state.dismiss = true
return .none
case .getWeatherData:
return .run { [state] send in
await send(
.weatherResponse(
Result {
try await self.weatherClient.getWeatherData(state.cityModel)
}
)
)
}
case .weatherResponse(.success(let model)):
state.isLoading = false
state.weatherModel = model
return .none
case .weatherResponse(.failure(let error)):
state.isLoading = false
state.alertState = AlertState { TextState(error.localizedDescription) }
return .none
}
}
.ifLet(\.alertState, action: \.alertAction)
}
} WeatherViewstruct WeatherDetailView: View {
@Bindable var store: StoreOf<WeatherDetailFeature>
@Environment(\.dismiss) var dismiss
var body: some View {
Group {
if store.isLoading {
ProgressView("\(store.cityModel.name) Weather Searching...")
} else {
if let model = store.weatherModel?.daily {
ScrollView {
VStack(alignment: .leading, spacing: 10) {
Text(store.cityModel.name)
.font(.title)
.fontWeight(.bold)
ForEach(0..<model.time.count, id: \.self) { index in
VStack(alignment: .leading) {
Text("\(model.time[index])")
.font(.title3)
Text("\(model.tempMax[index], specifier: "%.1f")°C")
.font(.headline)
Text("\(model.tempMin[index], specifier: "%.1f")°C")
.font(.headline)
}
.padding(.bottom, 5)
}
}
.padding()
}
} else {
Text("No Data")
}
}
}
.alert($store.scope(state: \.alertState, action: \.alertAction))
.onAppear {
store.send(.getWeatherData)
}
.onChange(of: store.dismiss) { _, _ in
dismiss()
}
}
} The logs for the Model are printed from the API function, but there is no response in the case .weatherResponse, neither fail nor success. its TCA Print Log received action:
WeatherDetailFeature.Action.getWeatherData
(No state changes)
received action:
SearchFeature.Action.searchQueryChange("re")
(No state changes)
received action:
SearchFeature.Action.searchQueryChange("rere")
SearchFeature.State(
_alertState: nil,
_isLoading: false,
_cityList: […],
- _searchQuery: "rer"
+ _searchQuery: "rere"
)
received action:
WeatherDetailFeature.Action.getWeatherData
(No state changes)
received action:
SearchFeature.Action.searchQueryChange("rere")
(No state changes) all code |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 2 replies
-
Hi @DeokHo98 That's not the navigation approach that TCA aims for. When examining your code, I see that the reducer is initialized in the view, which doesn't follow a tree-based or stack-based navigation method. In practice, every time the parent reducer State changes(SearchFeature), the reducer and store initialization are repeatedly called. To prevent this, it's better to compose the store for declaring the view in the parent reducer. |
Beta Was this translation helpful? Give feedback.
Hi @DeokHo98
That's not the navigation approach that TCA aims for. When examining your code, I see that the reducer is initialized in the view, which doesn't follow a tree-based or stack-based navigation method. In practice, every time the parent reducer State changes(SearchFeature), the reducer and store initialization are repeatedly called.
To prevent this, it's better to compose the store for declaring the view in the parent reducer.