Hi everyone,
I'm displaying a list of items that pushes another view when a row is tapped using @PresentationState
and .navigationDestination
but I noticed that sometimes when tapping a row nothing happens until a second tap is done like the following image.
Below I pasted a simplified version of what I'm trying to build that has the random behavior. Since I want to do different things when different parts of a row are tapped it makes sense to me to define the tap actions in the reducer (Row
) of each row. Then in my list reducer (ItemsList
) I simply set the PresentationState
when a specific part of the row is tapped.
To replicate the issue I go back and forward tapping different rows and eventually some of the taps will fail until another tap which could be in other part of the screen is done.
import ComposableArchitecture
import SwiftUI
@main
struct Application: App {
let items = ItemsList.State(
items: [
Row.State(name: "Item 1"),
Row.State(name: "Item 2"),
Row.State(name: "Item 3"),
]
)
var body: some Scene {
WindowGroup {
NavigationStack {
ItemsListView(
store: Store(
initialState: items,
reducer: { ItemsList() }
)
).navigationTitle("Presentation state")
}
}
}
}
struct Row: Reducer {
struct State: Equatable, Identifiable {
var id = UUID()
var name: String
}
enum Action {
case specificAreaTapped
}
var body: some Reducer<State, Action> { EmptyReducer() }
}
struct RowView: View {
let store: StoreOf<Row>
var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
HStack {
Text(viewStore.name)
Spacer()
Image(systemName: "chevron.right")
}
.contentShape(Rectangle())
.onTapGesture {
viewStore.send(.specificAreaTapped)
}
}
}
}
struct ItemsList: Reducer {
struct State: Equatable {
var items: IdentifiedArrayOf<Row.State>
@PresentationState var push: Row.State?
}
enum Action {
case row(Row.State.ID, Row.Action)
case push(PresentationAction<Row.Action>)
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .row(let id, .specificAreaTapped):
guard let tappedItem = state.items[id: id] else {
return .none
}
print("\(tappedItem.name) tapped")
state.push = tappedItem
return .none
case .push:
return .none
}
}
.forEach(\.items, action: /Action.row) { Row() }
.ifLet(\.$push, action: /Action.push) { Row() }
}
}
struct ItemsListView: View {
let store: StoreOf<ItemsList>
var body: some View {
WithViewStore(store, observe: \.items) { viewStore in
VStack {
ForEachStore(store.scope(state: \.items, action: ItemsList.Action.row)) { store in
RowView(store: store).padding()
}
Spacer()
}
.navigationDestination(
store: store.scope(state: \.$push, action: { .push($0) })
) { store in
WithViewStore(store, observe: { $0 }) { detailViewStore in
Text(detailViewStore.name)
}
}
}
}
}
For my tests I tried in iOS 16, 17 with Xcode 14 and 15 beta-5 respectively and swift-composable-architecture
v1.0.0 and v1.2.0 which was released today , with the same results.
Does anyone know what I'm doing wrong here or a reason for this behavior?
Additionally, I have some interesting findings:
- If I move the tap action from the child reducer to the list reducer everything works as expected, this is ok if there is only one tap action for the entire row.
- When adding
_printChanges
toItemsList
reducer to debug state's changes everything works as usual, the error is no longer there