Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DismissEffect not working in combination with NavigationStack and await store.send(...).finish() in stack's root view #3527

Open
2 of 3 tasks
chwo opened this issue Dec 10, 2024 · 2 comments
Labels
bug Something isn't working due to a bug in the library.

Comments

@chwo
Copy link

chwo commented Dec 10, 2024

Description

In the provided sample project, the app's root view's task action, the RootFeature, appends a feature state (DetailFeature) to the navigation path on the app's NavigationStack. When pressing the DetailFeature's close button, it calls the DismissEffect. The detail view, however, doesn't get dismissed. I figured out this is because the NavigationStack's root view (HomeView) awaits the send action to finish in the task view modifier.

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

The view pushed onto the NavigationStack should pop when calling dismiss.

Actual behavior

The view is not getting removed from the stack.

Reproducing project

Sample project: dismiss.zip

The dismiss is not working when using .task { await store.send(.task).finish() } in HomeView (which is the NavigationStack root view):

received action:
  RootFeature.Action.navigation(
    .path(
      .element(
        id: #0,
        action: .detail(.closeButtonTapped)
      )
    )
  )
  (No state changes)

The dismiss only works when just using .task { store.send(.task) } in HomeView.

received action:
  RootFeature.Action.navigation(
    .path(
      .element(
        id: #0,
        action: .detail(.closeButtonTapped)
      )
    )
  )
  (No state changes)

received action:
  RootFeature.Action.navigation(
    .path(
      .popFrom(id: #0)
    )
  )
  RootFeature.State(
    _navigation: NavigationFeature.State(
      _root: .home(…),
      _path: [
-       #0: .detail(
-         DetailFeature.State(
-         )
-       )
      ]
    )
  )

received action:
  RootFeature.Action.navigation(
    .root(
      .home(.task)
    )
  )
  (No state changes)

The Composable Architecture version information

1.17.0

Destination operating system

iOS 18

Xcode version information

Xcode version 16.0 (16A242d)

Swift Compiler version information

swift-driver version: 1.115 Apple Swift version 6.0 (swiftlang-6.0.0.9.10 clang-1600.0.26.2)
Target: arm64-apple-macosx15.0
@chwo chwo added the bug Something isn't working due to a bug in the library. label Dec 10, 2024
@pyrtsa
Copy link
Contributor

pyrtsa commented Dec 16, 2024

We are also experiencing this issue and I really don't think using await store.send(.task).finish() in one view should cause DismissEffect to fail in another! It seems to me like a bug in TCA navigation that such actions at a distance can occur at all.

At least I think we need better documentation and test coverage on the semantics of cancellation with the navigation utilities. The current implementation seems to set up and remove cancellation handlers quite (overly?) deliberately1.

While studying the issue, I found at least one workaround which seems to fix the above sample project, but I don't fully understand why:

// RootFeature.swift
@_spi(Internals) import ComposableArchitecture

...

switch action {
case .task:
    @Dependency(\.stackElementID) var stackElementID
    return .send(.navigation(.path(.push(
        id: stackElementID(),
        state: .detail(DetailFeature.State())
    ))))
...
}

Footnotes

  1. For example, why is the same cancellable added and removed to each navigation path prefix like that? And why are they called prefixes when the implementation actually returns suffixes by using dropFirst(index)?

@OguzYuuksel
Copy link

OguzYuuksel commented Dec 19, 2024

When deep linked instead of using task in the RootView we see same result.
but if we add some delay to the task in RootView then it will work.

        .task {
            try? await Task.sleep(for: .seconds(1))
            await store.send(.task).finish()
        }

I couldn't find time to investigate deeper but I can clearly see idsAfter and idsBefore are same in _StackReducer.reduce function when closeButtonTapped is called in the demo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working due to a bug in the library.
Projects
None yet
Development

No branches or pull requests

3 participants