Skip to content

@t0maboro/split stack columns poc - #4666

Draft
t0maboro wants to merge 8 commits into
mainfrom
@t0maboro/split-stack-columns-poc
Draft

t0maboro wants to merge 8 commits into
mainfrom
@t0maboro/split-stack-columns-poc

Conversation

@t0maboro

@t0maboro t0maboro commented Sep 16, 2026 •

Copy link
Copy Markdown
Member

Test with TestSplitStackColumns. I'm dumping all commits from PoC based on the RFC https://github.com/software-mansion/react-native-screens-labs/pull/1805 (option B). The plan is to create a single PR for each commit from here.


A Split column becomes a navigation stack of screens with Stack v5 semantics. Nothing between Split.Host and the screens is a React view, so the problem of hanging views from #4602 doesn't happen.
Proposed API for this option:

<Split.Host>
  <Split.Column>
    <Split.Stack>
      {screens.map(screen => (
        <Split.Screen
          key={screen.key}
          screenKey={screen.key}
          activityMode={screen.activityMode}
          onDismiss={() => remove(screen.key)}>
          <Split.HeaderConfig title={screen.key} />
          <Content />
        </Split.Screen>
      ))}
    </Split.Stack>
  </Split.Column>
  <Split.Column>{/* plain one-screen column, as today */}</Split.Column>
</Split.Host>

One commit per step, in the order of the RFC's implementation plan:

  1. and 2. Already on landed on main
  2. ios/stack/header moves to ios/header, RNSStackHeader* becomes RNSHeader*. Android header should be aligned.
  3. Stack core logic lifted out of Stack.Host and typed on RNSStackScreenProviding.
  4. Header coordinator works on any UIViewController; the header config resolves its screen through the protocol instead of asserting on the Stack screen class.
    6 RNSSplitColumnController (manager, not VC), a native object with no React counterpart which owns the column's navigation controller and its frame observer and replaces RNSSplitNavigationController.
  5. A Split.Column with a Split.Stack child renders no native view and only provides its column id through context; the host routes each mounted screen to its column; the column controller drives RNSStackOperationCoordinator over UIView<RNSStackScreenProviding> the way Stack.Host does.
  6. Split.HeaderConfig renders the neutral header config; RNSHeaderCoordinator owned by RNSSplitScreenController applies it to the column's navigation item.

t0maboro and others added 8 commits September 8, 2026 13:12
The header config, its items and spacers and the per-screen header coordinator are used by
every container that hosts a stack of screens, so they stop carrying the `Stack` name and living
under `ios/stack`: `RNSStackHeader*` becomes `RNSHeader*` in `ios/header`,
`RNSStackScreenHeaderCoordinator` becomes `RNSHeaderCoordinator`, the iOS Fabric components are
`RNSHeaderConfigIOS`, `RNSHeaderItemIOS` and `RNSHeaderItemSpacerIOS` (specs in
`src/fabric/header`), and the shared C++ shadow nodes follow. The Android header stays a Stack
feature: `RNSStackHeaderConfigAndroid`, `RNSStackHeaderSubview` and their Kotlin side are
untouched. Mechanical rename, no behavior change; the public JS API of `Stack.HeaderConfig` is
unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C43Zo2sKs9JZ7GfJJGLjyg
…elpers/stack and type it on RNSStackScreenProviding

`RNSStackNavigationController` with its navigation bar and bar coordinator, `RNSStackOperation`,
`RNSStackOperationCoordinator` and `RNSViewFrameChangeDelegate` are the machinery any container
needs to host a stack of screens, not a part of the `Stack.Host` component; they move to
`ios/helpers/stack` (the SPM header search paths gain the directory).

The coordinator, the operations and the navigation controller take their screens as
`UIView<RNSStackScreenProviding>` instead of `RNSStackScreenComponentView`, so that any React view
whose controller can live on the stack's navigation controller may be a stack item. The protocol
lives next to that machinery and `RNSStackScreenActivityMode` moves to its header. The navigation
controller resolves the content scroll view of its top screen through `RNSContainerItem` instead of
the `RNSStackScreenController` class, which is the only Stack v5 type it still referenced. Types
only, no behavior change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C43Zo2sKs9JZ7GfJJGLjyg
…dent of the screen class

`RNSStackScreenHeaderCoordinator` works on any `UIViewController` (it only touches the navigation
item and the navigation controller), `RNSStackScreenProviding` exposes the screen's header
coordinator, and `RNSStackHeaderConfig` resolves its screen through the protocol instead of
asserting on `RNSStackScreenComponentView`. No behavior change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C43Zo2sKs9JZ7GfJJGLjyg
…mn navigation controller

`RNSSplitColumnController` is a native object without a React counterpart: it creates the
navigation controller installed in a UISplitViewController column, observes the origin of its
view through `RNSSplitColumnFrameObserver` and reports it to the host. The host creates one
controller per column on the first update and only swaps their root screen controllers
afterwards, instead of rebuilding navigation controllers on every children update.

`RNSSplitNavigationController` and its frame origin delegate are replaced by the column
controller and its delegate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C43Zo2sKs9JZ7GfJJGLjyg
Every Split column is a navigation stack of `RNSSplitScreen`s. `Split.Column` with a `Split.Stack`
child renders no native view: it only provides the column index through context, and its
`Split.Screen`s (prop `column`) mount directly in the host. A plain `Split.Column` is a one-screen
column as before. Nothing between the host and the screens is a React view, so no view of the
React tree hangs outside the UIKit hierarchy.

Natively, `RNSSplitColumnController` owns the column's `RNSStackNavigationController` and drives
its stack the way `RNSStackHostComponentView` drives a standalone stack, through
`RNSStackOperationCoordinator` over the screens as `UIView<RNSStackScreenProviding>`. The host
routes each mounted screen to its column by index; screens mounted before the host controller
exists (it is created on `didMoveToWindow`) are replayed then. A screen popped natively reports
`onDismiss({ isNativeDismiss: true })`; its React unmount does not pop it again, because a pop is
only requested for a screen still on the column's navigation stack.

The column frame reported to the shadow tree comes from the navigation controller's view, not
from the screen view UIKit animates during transitions, and the screen controller is released a
turn after `invalidate` so that a pop pending after the mounting transaction still finds it.

JS API: `Split.Stack`, `Split.Screen` (`activityMode`, `screenKey`, `onDismiss`), scenario
`test-split-stack-columns-ios` following the Stack detach protocol (detach, then unmount on
`onDismiss`).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C43Zo2sKs9JZ7GfJJGLjyg
`RNSSplitScreen` accepts `RNSHeaderConfig` exactly like `RNSStackScreen`: the config is a subview
of the screen, its items stay out of the view hierarchy, and `RNSHeaderCoordinator`, now owned by
`RNSSplitScreenController` and exposed through `RNSStackScreenProviding`, applies it to the
screen's navigation item in the column's `RNSStackNavigationController`. Without a header config
a column keeps the system navigation bar as before.

JS API: `Split.HeaderConfig` with its own `SplitHeaderConfigProps`; it renders the same neutral
header config as `Stack.HeaderConfig`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C43Zo2sKs9JZ7GfJJGLjyg
@coderabbitai

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
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.

1 participant