isPresented is showing true for an embedded feature
Closed this issue · 1 comments
Description
I might be stumbled upon a bug with the isPresented
utility but I'm unsure whether this actually might be the intended behaviour.
One of my features in my app can be used as a presented view (i.e. a sheet) as well as being embedded into a preexisting view.
I utilize dismiss
in combination with 'isPresented' to dismiss this feature through a cancel button.
This view is used a tree navigation hierarchy and thus, even if embedded, can be part of a presented view marked with @Presents
higher up in the view hierarchy.
For clarity sake, let's use the following naming and hierarchy:
RootFeature -> @presents FullScreenFeature (fullscreenmodal) -> EmbeddedFeature (as a conditional child view)
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
I expect the isPresented
of the EmbeddedFeature to be true, if the feature is utilised behind a @Presents
annotation, while it should be false without the annotation. This should also be the case if the FullScreenFeature, that embeds the EmbeddedFeature, is presented via @Presents
by the RootFeature.
Actual behavior
Unfortunately, the isPresented
within the EmbeddedFeature is true in the aforementioned scenario:
RootFeature -> @presents FullScreenFeature (fullscreenmodal) -> EmbeddedFeature (as a conditional child view)
leading to a dismissal of the Fullscreenfeature upon tapping the cancel button in the EmbeddedFeature.
Steps to reproduce
import SwiftUI
import ComposableArchitecture
@main
struct MyApp: App {
let store = Store(initialState: RootFeature.State(), reducer: { RootFeature() })
var body: some Scene {
WindowGroup {
RootView(store: store)
}
}
}
@Reducer
struct RootFeature {
@ObservableState
struct State {
@Presents var destination: Destination.State?
}
@Reducer(state: .equatable)
enum Destination {
case fullscreen(FullScreenFeature)
}
enum Action {
case fullScreenButtonTapped
case destination(PresentationAction<Destination.Action>)
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .fullScreenButtonTapped:
state.destination = .fullscreen(.init())
return .none
default: return .none
}
}
.ifLet(\.$destination, action: \.destination)
}
}
struct RootView: View {
@Bindable var store: StoreOf<RootFeature>
var body: some View {
Button("Full Screen", action: { store.send(.fullScreenButtonTapped) })
.fullScreenCover(
item: $store.scope(state: \.destination?.fullscreen, action: \.destination.fullscreen),
content: { FullScreenView(store: $0) }
)
}
}
@Reducer
struct FullScreenFeature {
@ObservableState
struct State: Equatable {
var embeddedFeature: EmbeddedFeature.State? = nil
}
enum Action {
case embeddedViewButtonTapped
case embeddedFeature(EmbeddedFeature.Action)
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .embeddedViewButtonTapped:
state.embeddedFeature = .init()
return .none
case .embeddedFeature(.hideButtonTapped):
state.embeddedFeature = nil
return .none
}
}
.ifLet(\.embeddedFeature, action: \.embeddedFeature) { EmbeddedFeature() }
}
}
struct FullScreenView: View {
let store: StoreOf<FullScreenFeature>
var body: some View {
Button("Show Embedded View", action: { store.send(.embeddedViewButtonTapped) })
if let embeddedFeatureStore = store.scope(state: \.embeddedFeature, action: \.embeddedFeature) {
EmbeddedView(store: embeddedFeatureStore)
}
}
}
@Reducer
struct EmbeddedFeature {
@Dependency(\.dismiss)
private var dismiss
@Dependency(\.isPresented)
private var isPresented
@ObservableState
struct State: Equatable {
}
enum Action {
case hideButtonTapped
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .hideButtonTapped:
if isPresented {
return .run { _ in await dismiss() }
}
return .none
}
}
}
}
struct EmbeddedView: View {
let store: StoreOf<EmbeddedFeature>
var body: some View {
VStack(spacing: 10) {
Text("Embedded View 🎉")
Button("Hide Embedded View", action: { store.send(.hideButtonTapped) })
}
.padding()
.background(.teal)
}
}
The Composable Architecture version information
No response
Destination operating system
No response
Xcode version information
No response
Swift Compiler version information
No response
Hi @Narsail, your FullScreenFeature
reducer is not using the presentation tools of the library. It needs to use @Presents
and PresentationAction
in order for \.dismiss
to work.
Since this isn't an issue with the library I am going to convert it to a discussion. Please feel free to ask any questions over there!