Skip to content

feat(iOS, Tabs): integration with ScrollViewMarker#4191

Open
kkafar wants to merge 14 commits into
mainfrom
@kkafar/svm-tabs-integration
Open

feat(iOS, Tabs): integration with ScrollViewMarker#4191
kkafar wants to merge 14 commits into
mainfrom
@kkafar/svm-tabs-integration

Conversation

@kkafar

@kkafar kkafar commented Jun 18, 2026

Copy link
Copy Markdown
Member

Description

Integrates the ScrollViewMarker component with the Tabs special-effects system on iOS.

Until now, Tabs relied purely on a descendant-chain heuristic (RNSScrollViewFinder) to
locate the scroll view backing a tab screen. This PR lets a ScrollViewMarker explicitly
register its wrapped scroll view with the enclosing tab screen, so the content scroll view
is known precisely instead of being guessed. The heuristic is kept as a fallback when no
marker is present.

This integration is currently gated behind the RNS_GAMMA_ENABLED compilation flag and will
remain so until we are able to remove that flag.

Closes https://github.com/software-mansion/react-native-screens-labs/issues/1536
Closes https://github.com/software-mansion/react-native-screens-labs/issues/1537

Changes

  • RNSTabsScreenComponentView now conforms to RNSScrollViewSeeking. When a descendant
    marker registers its scroll view, the screen forwards it to its view controller via
    setContentScrollView:forEdge:NSDirectionalRectEdgeAll. On a tab change the
    RNSTabBarController queries the child view controller for its content scroll view, so the
    registered scroll view flows into the special-effects system automatically.
  • RNSTabsScreenViewController gains resolveContentScrollView, which prefers the explicitly
    registered content scroll view (querying the bottom, then top edge) and falls back to the
    RNSScrollViewFinder heuristic. The repeated-tab-selection scroll-to-top effect now uses it.
  • RNSScrollViewMarkerComponentView:
    • walks the React superview chain (reactSuperview) when looking for a seeking ancestor,
      because at transaction-end time the RNSTabsScreenComponentView may not yet be attached to
      its parent (tab not selected, or registration runs too early);
    • resets its registration state when it leaves the window, so it re-registers on
      re-attachment (e.g. switching back to a tab).
  • Cleanup in RNSTabsHostComponentView.
  • Added an integration test scenario exercising a marker inside Tabs.

Notes / limitations

  • Only the case where the ScrollViewMarker sits directly under Tabs (no Stack v4/v5 in
    between) is handled here. Those compositions will be integrated in follow-up PRs.
  • With multiple ScrollViewMarkers under the same seeking ancestor there is currently a
    potential race condition in which marker wins registration.

Visual docs

Edge effects

Important

Seemingly the edge effect is applied correctly no matter the integration with the scrollview marker. Likely UITabBarController uses some different logic to UIViewController.contentScrollView to detect scrollview, because even when I have had returned nil from the method, the edge effect had still been applied. Nevertheless, I decided to include this test case here, just to make sure it works. Also this allows us to test that different ScrollViewMarkers from different tab update the edge effect correctly on tab change.

before after
ios-26-scroll-edge-effects-before.mov
ios-26-scroll-edge-effects-after.mov

Special effects

before after
ios-26-tabs-special-effects-before.mov
ios-26-tabs-special-effects-after.mov

Test plan

Tested with the new integration scenario:

  • apps/src/tests/component-integration-tests/scroll-view-marker/test-svm-tabs-edge-effects/index.tsx
  • apps/src/tests/component-integration-tests/scroll-view-marker/test-svm-tabs-edge-effects/scenario-description.ts
  • registered in apps/src/tests/component-integration-tests/index.tsx as ScrollViewMarker

Steps (iOS, RNS_GAMMA_ENABLED=1):

  1. Run FabricExample and open the component-integration tests.
  2. Open ScrollViewMarker integration casesIntegration: SVM in Tabs - edge effects.
  3. Scroll the list in each tab and confirm the scroll edge effect is applied per tab
    (Homehard, Secondsoft).
  4. Tap the currently selected tab again and confirm the list scrolls to top (repeated-tab
    selection effect resolves the marker-registered scroll view).

Checklist

  • Included code example that can be used to test this change.
  • For visual changes, included screenshots / GIFs / recordings documenting the change.
  • For API changes, updated relevant public types.
  • Ensured that CI passes

kkafar added 5 commits June 18, 2026 17:29
There are a couple of important things here.
First, note that we set the *contentScrollView* on the
`RNSTabsScreenViewController`. This works, because on tab change the
`RNSTabBarController` queries the child view controller for its *content
scroll view*.

Second - I've improved search method for `RNSScrollViewSeeking`, since
at the moment of transaction end, the `RNSTabsScreenComponentView` is
not yet attached to it's parent - it's either not currently selected or
it's just too early. This way I give a change to find a seeking parent.
Please note however, that if there were multiple `ScrillViewMarkers`,
then we'd have a kind of race condition.
Thanks to how we've structured this beforehand, it was easy!

There are things to note, however. First, this commit handles only case
where the scrollview marker is under Tabs, but there is no Stack v4 or
v5 between. These will be integrated separately in separate PRs.
@kkafar kkafar force-pushed the @kkafar/svm-tabs-integration branch from e94b623 to 93ed840 Compare June 18, 2026 15:30
@kkafar kkafar requested a review from Copilot June 18, 2026 16:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to integrate iOS Tabs with ScrollViewMarker so that Tabs-related behaviors (e.g., resolving the “content” scroll view for special effects / edge-related behavior) work even when the scroll view isn’t discoverable via the existing first-descendant-chain heuristic. It also adds a new component-integration scenario in the example app to exercise ScrollViewMarker-in-Tabs behavior.

Changes:

  • Update iOS Tabs screen controller to resolve the content scroll view via a new helper method.
  • Add RNSScrollViewSeeking plumbing in RNSTabsScreenComponentView to receive marker-provided scroll views.
  • Add a new component-integration test scenario group + scenario for ScrollViewMarker in Tabs (iOS).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
ios/tabs/screen/RNSTabsScreenViewController.mm Adds a helper to resolve the content scroll view for repeated tab-selection behavior.
ios/tabs/screen/RNSTabsScreenComponentView.mm Adds ScrollViewMarker “seeking” integration to pass a descendant scroll view upward.
ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.mm Adjusts ancestor traversal to use React superview semantics and resets registration when removed from window.
apps/src/tests/component-integration-tests/scroll-view-marker/test-svm-tabs-edge-effects/scenario-description.ts Introduces metadata for a new integration scenario.
apps/src/tests/component-integration-tests/scroll-view-marker/test-svm-tabs-edge-effects/index.tsx Adds a new integration scenario app exercising ScrollViewMarker inside Tabs.
apps/src/tests/component-integration-tests/scroll-view-marker/index.ts Registers the new ScrollViewMarker integration scenarios group.
apps/src/tests/component-integration-tests/index.tsx Exposes the new ScrollViewMarker scenario group in the component-integration test menu.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ios/tabs/screen/RNSTabsScreenViewController.mm
Comment thread ios/tabs/screen/RNSTabsScreenComponentView.mm
Comment thread ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.mm
kkafar added 3 commits June 22, 2026 12:00
RNSTabsScreenComponentView stored the registered scroll view and its
marker in ivars that were only ever assigned and nilled, never read.
The marker was held strongly even though it lives inside the screen's
own view subtree, adding a redundant strong edge to a descendant.

Remove both ivars and their assignments; registerDescendantScrollView:
now just forwards the scroll view to the controller.
Add a second ScrollViewMarker integration scenario covering the
tab-repetition special effect with a Stack nested inside Tabs, and
register it in the scenario group.

HeuristicBreakingView is now an actual heuristic-breaking element
(non-collapsable, zero-height view rendered before the marker) so both
scenarios exercise the marker-based content-scroll-view registration
path instead of falling back to the descendant-chain finder. Also drop
unused imports left over in the edge-effects scenario.
Clarify that the scenario covers scroll edge effects specifically.
Renames the directory, scenario key, and import symbol.
@kkafar kkafar marked this pull request as ready for review June 22, 2026 12:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

import type { ScenarioDescription } from '@apps/tests/shared/helpers';

export const scenarioDescription: ScenarioDescription = {
name: 'Integration: SVM in Tabs - edge effects',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: do we need the "Integration:" in the name if it's already in component integration tests directory? Applies to both test screens.

Comment on lines +108 to +113
if (auto sv = [self contentScrollViewForEdge:NSDirectionalRectEdgeBottom]; sv != nil) {
return sv;
}
if (auto sv = [self contentScrollViewForEdge:NSDirectionalRectEdgeTop]; sv != nil) {
return sv;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why we check bottom and then top edge in this order or is it arbitrarily chosen? It would be nice to have a comment here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I don't think there is. There might be once we add possibility to specify multiple scrollviews (different scrollviews for different edges), but it is not yet supported.

{
[super didMoveToWindow];
if (self.window == nil) {
_hasAttemptedRegistration = NO;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The marker immediately re-registers the scroll view after any mounting transaction:

Screen.Recording.2026-06-23.at.09.33.35.mov

Should we gate registration with window != nil check maybe?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That won't do unfortunately. But I think I'll try to organise the registration in a bit different manner.

@t0maboro

t0maboro commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

I'm seeing some inconsistency: after the 1st tab switch, scrollToTop isn't working, after subsequent switches it started working

stack-svm-tabs.mov

return <TabsContainer routeConfigs={TABS_ROUTE_CONFIGS} />;
}

function StackTabScreen() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aren't we missing export or is this intentional?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we are coming back to state where we will export only App() (now it should be renamed) and default export createScenario()
#4201

@LKuchno LKuchno left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a few comments with suggested changes to better align with our current test and scenario structures. Also changes corresponding to new export/import PR#4201 changes were propose. Some comments include open questions regarding the correct naming conventions.

Comment thread apps/src/tests/component-integration-tests/scroll-view-marker/index.ts Outdated
Comment thread apps/src/tests/component-integration-tests/index.tsx Outdated
Comment thread apps/src/tests/component-integration-tests/index.tsx Outdated
return <TabsContainer routeConfigs={TABS_ROUTE_CONFIGS} />;
}

function StackTabScreen() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we are coming back to state where we will export only App() (now it should be renamed) and default export createScenario()
#4201


- [ ] There should be two tabs: `Home` and `Second`.
- [ ] `Home` tab should be selected.
- [ ] `Hard` scroll-edge-effect should be applied (opqaue background of tab bar).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can expand a bit on what is expected for Hard:

Suggested change
- [ ] `Hard` scroll-edge-effect should be applied (opqaue background of tab bar).
- [ ] `Hard` scroll-edge-effect should be applied (opaque background of tab bar with a hard cutoff and dividing line).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dividing line? I haven't noticed such. Will take another look later 😄

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's actually taken from 'hard' scroll-edge-effect documentation I interpret it as visible cutoff tab bar 😅

Co-authored-by: Krzysztof Ligarski <63918941+kligarski@users.noreply.github.com>
Co-authored-by: lkuchno <45803783+LKuchno@users.noreply.github.com>

## Prerequisites

iOS: simulator with iOS 26+ is enough,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: ,

- [ ] There should be two tabs: `Home` and `Stack`.
- [ ] `Home` tab should be selected.

2. Scrolldown a bit.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: scroll down

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.

6 participants