Description
Description
Hey 👋
I have an issue with dismissing screens from reducers on iOS 16.
I use a tree based navigation in my project. The problem happens only on iOS 16.X and only when I drill down at least 2 times. I tried 2 approaches:
- using
@Dependency(\.dismiss) var dismiss
in the current reducer - delegating dismissal to parent reducer, and then parent setting its state.destination to nil
Neither of them works for me. It looks like the reducer is removed from the navigation tree properly, but then NavigationStack fails to refresh and empties the current screen without pop. Here's a video demonstrating the problem:
https://github.com/user-attachments/assets/5b861536-e179-4184-b36b-b986050f2169
Funnily enough, it works properly if we drill down only once:
https://github.com/user-attachments/assets/2626e88a-0e3c-4dc9-9f9d-a5526c9ca6ce
Checklist
- I have determined whether this bug is also reproducible in a vanilla SwiftUI project.
- If possible, I've reproduced the issue using the
main
branch of this package. - This issue hasn't been addressed in an existing GitHub issue or discussion.
Expected behavior
When drilling down in a tree based navigation at least 2 times and then using dismiss() from reducer, it should pop the current screen from the NavigationStack.
Here is a recording of the same code working as expected on iOS 17:
https://github.com/user-attachments/assets/75b5227e-f762-4c10-8ecc-58d09ddf65f8
Actual behavior
When drilling down in a tree based navigation at least 2 times and then using dismiss() from reducer, it empties the view of latest screen. After using native Navigation bar back button from this state, it pops the screen, but the previous one is unresponsive. Only after popping again using native navigation bar it starts working properly again.
Reproducing project
Here's the code used in the recordings:
import ComposableArchitecture
import SwiftUI
@main
struct TestApp: App {
var body: some Scene {
WithPerceptionTracking {
WindowGroup {
NavigationStack {
TreeNavigationView(store: .init(initialState: .init(), reducer: {
TreeNavigation()
}))
}
}
}
}
}
struct TreeNavigationView: View {
@Perception.Bindable var store: StoreOf<TreeNavigation>
var body: some View {
WithPerceptionTracking {
Form {
Button("Show nested") { store.send(.showNested) }
Button("Dismiss") { store.send(.dismiss) }
}
.navigationDestination(item: $store.scope(state: \.destination?.nested, action: \.destination.nested)) { store in
WithPerceptionTracking {
TreeNavigationView(store: store)
}
}
}
}
}
@Reducer
struct TreeNavigation {
@Dependency(\.dismiss) var dismiss
@Reducer(state: .equatable)
enum Destination {
case nested(TreeNavigation)
}
@ObservableState
struct State: Equatable {
@Presents var destination: Destination.State?
}
enum Action {
case destination(PresentationAction<Destination.Action>)
case showNested
case dismiss
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .showNested:
state.destination = .nested(.init())
// Alternative approach using parent to nil its destination
// case .destination(.presented(.nested(.dismiss))):
// state.destination = nil
// case .dismiss:
// break
case .dismiss:
return .run { _ in await dismiss() }
case .destination:
break
}
return .none
}
.ifLet(\.$destination, action: \.destination)
}
}
Available also in form of a complete mini project:
NavTest.zip
The Composable Architecture version information
1.17.1
Destination operating system
iOS 16.X
Xcode version information
Version 15.4 (15F31d)
Swift Compiler version information
swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4)
Target: arm64-apple-macosx14.0