Skip to content

Conversation

stephencelis
Copy link
Member

Tapping the back button or interactively popping via gesture can conflict with observation when the view controller being popped to executes a mutation in its will- or didAppear. While this isn't common in vanilla UIKit, in TCA sending even a no-op action in these lifecycle hooks can lead to immediately re-pushing the view controller that was just popped.

This commit does what it can to detect when the back button was pushed or the interactive pop gesture was invoked so that if/when the pop is committed, we can update the path more eagerly, avoiding the re-presentation.

Tapping the back button or interactively popping via gesture can
conflict with observation when the view controller being popped to
executes a mutation in its `will`- or `didAppear`. While this isn't
common in vanilla UIKit, in TCA sending even a no-op action in these
lifecycle hooks can lead to immediately re-pushing the view controller
that was just popped.

This commit does what it can to detect when the back button was pushed
or the interactive pop gesture was invoked so that if/when the pop is
committed, we can update the path more eagerly, avoiding the
re-presentation.
@stephencelis
Copy link
Member Author

@mbrandonw Would be nice to write some tests for this but not sure how easy it will be given:

  1. I think it requires the path live in some other observable state in TCA
  2. Unit testing against the back button being pressed, or the interactive pop gesture, seems maybe possible, but could be tricky to do so in a real-world kind of way

@7hommay
Copy link

7hommay commented Aug 8, 2025

Hi, I think I'm running into the issue this PR is trying to fix, when using the iOS 18+ zoom transition in UIKit.

I've modified the initializer of StaticNavigationStackController in the "Static Path" case study to be similar to my situation like so:

  convenience init(model: Model) {
    @UIBindable var model = model
    self.init(path: $model.path) {
      RootViewController(model: model)
    }
    self.navigationDestination(for: Model.Path.self) { path in
      let controller = {
        switch path {
        case .feature1:
          FeatureViewController(number: 1)
        case .feature2:
          FeatureViewController(number: 2)
        case .feature3:
          FeatureViewController(number: 3)
        }
      }()
      
      if #available(iOS 18, *) {
        controller.preferredTransition = .zoom(options: nil, sourceViewProvider: { _ in
          nil // Not providing a view here for simplicity, the issue can be triggered without 
        })
      }
      
      return controller
    }
    self.model = model
  }

When dismissing a controller by swiping, or just pressing the back button, and pushing a new controller while the transition is still running will re-push the currently transitioning controller. When actually providing a source view, like a cell in a collection view in my case, this is very easy to trigger. The transition is quite long. I've attached a recording of the modified StaticNavigationStackController example. I Hope this is helpful.

Simulator.Screen.Recording.-.iPhone.16.Pro.-.2025-08-08.at.18.28.20.mov

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants