Repository navigation
fix(ios): pop only the topmost React Native screen on popToNative - #470
Merged
hurali97 merged 2 commits intoSep 24, 2026
Merged
Conversation
popToNative() is posted as an app-wide NotificationCenter notification with no sender, so every live React Native surface acted on it — including ones buried under other screens. In a stack like RN -> native -> RN, one call tore down everything, taking the native screen in between with it. On top of that, a SwiftUI-hosted surface handled the notification twice: once in ReactNativeView via dismiss(), and again in the ReactNativeViewController it wraps via popViewController. BrownfieldSurfaceRegistry now picks a single surface — the most recently shown one that is currently on screen — and only that surface acts, once. Visibility is read live (view.window) rather than cached from viewWillAppear, because SwiftUI recreates hosted controllers and a cached flag is stale exactly when the notification arrives. The registry has no UIKit dependency, so it runs under `swift test` on macOS in the existing iOS native tests CI job. Fixes callstack#354 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hurali97
requested changes
Sep 23, 2026
Member
There was a problem hiding this comment.
This looks like an unusual place to put this and related files. Let's put this at packages/react-native-brownfield/ios/.
We can then discard the Package.swift change
Contributor
Author
There was a problem hiding this comment.
I followed the BrownfieldBundleURLResolver example. SPM can't reach outside its package root, so moving the file to ios will delete the unit tests. I can do that but just making sure we're fine with that.
For what it's worth they only cover the surface election logic, which is the simple part. The bugs I actually hit were all in the wiring and the e2e caught those, so losing them isn't a big deal.
…ources Per review, the registry sits in ios/ alongside ReactNativeBrownfield.swift rather than under ios/swiftpm/Sources, so the Package.swift change is no longer needed. SPM targets cannot reference sources outside the package root, so the registry's unit tests go with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hurali97
approved these changes
Sep 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #354.
popToNative()posts an app-wideNotificationCenternotification with no sender, so every live React Native surface acted on it — including ones sitting underneath other screens. With a stack like RN → native → RN, one call popped the lot and took the native screen in the middle with it.There was a second, smaller problem in the same path: a SwiftUI-hosted surface handled the notification twice, once in
ReactNativeViewviadismiss()and again in theReactNativeViewControllerit wraps viapopViewController. So even a single RN screen popped one level too far.BrownfieldSurfaceRegistrynow elects one surface — the most recently shown one that's actually on screen — and only that surface acts, once per host. This matches what Android already does implicitly, since its back dispatcher only ever delivers to the top callback.A couple of things worth knowing while reviewing:
view.window) rather than cached from the appearance callbacks. SwiftUI recreates hosted controllers, so a cached flag is stale exactly when the notification arrives — I had it the other way round first and it broke.popViewController, so the SwiftUI host still does the dismissing. The controller stands down and only steps in if the dismiss clearly did nothing, which is the case for aReactNativeViewinside aUIHostingControllerpushed onto a UIKit stack.Two behaviour changes are called out in the changeset:
popToNative()is now a no-op (with a log) when no RN surface is on screen, and it no longer dismisses a modally presented native screen that merely contains an RN surface.E2E coverage and a demo screen to exercise this are in a follow-up PR stacked on this one, to keep the fix reviewable on its own.
🤖 Generated with Claude Code