Skip to content

Comments

feat: add ScrollViewMarker experimental component#3674

Open
kkafar wants to merge 16 commits intomainfrom
@kkafar/content-scrollview-component
Open

feat: add ScrollViewMarker experimental component#3674
kkafar wants to merge 16 commits intomainfrom
@kkafar/content-scrollview-component

Conversation

@kkafar
Copy link
Member

@kkafar kkafar commented Feb 21, 2026

Description

This PR adds a PoC of ScrollViewMarker experimental native component.

It is introduced to handle two responsibilities:

  1. Configuration of ScrollView (in particular edge effects)

    I want to move these configuration options off the "screen components",
    and have a single component that can be used to configure the ScrollView.

  2. Detection of ScrollView in native view hierarchy

    Currently, both we & UIKit employ very simple "first descendant chain" heuristics
    to detect the presence of ScrollView in the view hierarchy & use it as
    "content ScrollView". This sometimes works, but it is very easy to break by
    user code.

    Now, user will be required to wrap the ScrollView in our component, so this
    is an additional step, but it will allow us to robustly detect the presence
    of ScrollView.

    There is open question of what do we do, if there are multiple ScrollViews
    a user want to configure. There can be only a single "content ScrollView".
    Atm. I think that reasonable approach is to have non-required isContentScrollView
    property with true as default value, and if there are multiple ScrollView instances,
    we can throw an error asking to set this property to false for all, but the
    actual "content ScrollView". We should decide on default value of isContentScrollView
    based on what we estimate as more frequent use case.

    Alternatively, we can expose two components: ScrollViewMarker & ContentScrollViewMarker,
    which will handle that prop configuration for the user? Let's discuss below.

Beside adding a component I've also implemented basic support for scroll edge effects
configuration.

RNSScrollViewSeeking protocol is introduced. Other component can implement it to be
notified when ScrollViewMarker is attached to view hierarchy. Currently it is not
used anywhere & it's shape is not final, but it serves PoC purposes.

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

Caveats

  • a84419e

    Similarly to iOS, I use UIManagerListener.didMountItems to detect a
    moment when the view hierarchy is assembled.

    [!caution]
    I've tested & views are created in bottom-up order -> that means the
    ScrolLViewMarker will usually be created BEFORE e.g. a StackHost,
    therefore its UIManagerListener listener will be added first -> it
    will be called before listener of StackHost. If for some reason order
    of view creation changes, e.g. ScrollViewMarker will be rendered in
    consecutive render, the order here will also change.

Visual documentation

N/A

Test plan

It's rather hard to test this component in isolation & somehow observe an effect
in JS. If someone has an idea - let me know.

Right now, I've introduced test-svm-configures-scroll-view, which uses
StackContainer to make scroll edge effects visible, but mostly this test is about prepared environment for testing on native side. Especially on Android, where there are no edge effects.

Checklist

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

It is still to be decided how ownership model will work
It uses `StackContainer`, but I don't see a way to test
this component in full isolation right now. Can we come up with
something?
Similarly to iOS, I use UIManagerListener.didMountItems to detect a
moment when the view hierarchy is assembled.

> [!caution]
> I've tested & views are created in bottom-up order -> that means the
> `ScrolLViewMarker` will usually be created BEFORE e.g. a `StackHost`,
> therefore its `UIManagerListener` listener will be added first -> it
> will be called before listener of `StackHost`. If for some reason order
> of view creation changes, e.g. `ScrollViewMarker` will be rendered in
> consecutive render, the order here will also change.
@kkafar kkafar changed the title [WIP] feat: add ScrollViewMarker experimental component feat: add ScrollViewMarker experimental component Feb 23, 2026
Copy link
Member Author

@kkafar kkafar left a comment

Choose a reason for hiding this comment

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

Notes for reviewers

Comment on lines +195 to +203
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[super mountChildComponentView:childComponentView index:index];
}

- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[super unmountChildComponentView:childComponentView index:index];
}
Copy link
Member Author

Choose a reason for hiding this comment

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

I've left these for easier debugging.

@kkafar kkafar marked this pull request as ready for review February 23, 2026 14:37
@kkafar kkafar requested review from Copilot, kligarski, kmichalikk and t0maboro and removed request for kligarski February 23, 2026 14:37
Copy link

Copilot AI left a comment

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 introduces a new experimental ScrollViewMarker component for React Native Screens. The component serves two main purposes: (1) configuring ScrollView properties, particularly edge effects on iOS 26+, and (2) providing a robust way to detect ScrollViews in the native view hierarchy through an explicit marker pattern, replacing the fragile "first descendant chain" heuristics.

Changes:

  • Added ScrollViewMarker experimental component with TypeScript and native (iOS/Android) implementations
  • Implemented scroll edge effect configuration support for iOS 26+
  • Introduced RNSScrollViewSeeking protocol for ScrollView registration and discovery
  • Added test scenario to verify ScrollViewMarker functionality with StackContainer

Reviewed changes

Copilot reviewed 20 out of 21 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/fabric/gamma/ScrollViewMarkerNativeComponent.ts Codegen native component spec defining props for scroll edge effects
src/experimental/types.ts Exports ScrollViewMarker types to experimental API
src/experimental/index.ts Exports ScrollViewMarker component to experimental API
src/components/gamma/scroll-view-marker/index.ts Main export file for ScrollViewMarker module
src/components/gamma/scroll-view-marker/ScrollViewMarker.types.ts TypeScript type definitions for component props
src/components/gamma/scroll-view-marker/ScrollViewMarker.tsx React component implementation mapping props to native component
package.json Registers native component class for iOS Fabric
ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.mm iOS native implementation with edge effect application and ScrollView detection
ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.h iOS header defining RNSScrollViewSeeking protocol
ios/conversion/RNSConversions.h Updated header guards to include ScrollViewMarker conversions
ios/conversion/RNSConversions-Stack.h Updated header guards for consistency
ios/conversion/RNSConversions-ScrollViewMarker.mm Conversion functions for edge effect enums
ios/conversion/RNSConversions-ScrollViewMarker.h Conversion function declarations
apps/src/tests/single-feature-tests/scroll-view-marker/test-svm-configures-scroll-view.tsx Test scenario demonstrating ScrollViewMarker with StackContainer
apps/src/tests/single-feature-tests/scroll-view-marker/index.ts Test scenario group registration
apps/src/shared/utils/color-generator.ts Utility for generating sequential colors in tests
android/src/main/java/com/swmansion/rnscreens/gamma/scrollviewmarker/ScrollViewSeeking.kt Android interface for ScrollView registration
android/src/main/java/com/swmansion/rnscreens/gamma/scrollviewmarker/ScrollViewMarkerViewManager.kt Android ViewManager with iOS-only prop stubs
android/src/main/java/com/swmansion/rnscreens/gamma/scrollviewmarker/ScrollViewMarker.kt Android native implementation with ScrollView detection
android/src/main/java/com/swmansion/rnscreens/RNScreensPackage.kt Registers ScrollViewMarkerViewManager
FabricExample/ios/Podfile.lock Updated dependency checksums

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

// scrollView is a ViewGroup, because there is no universal ScrollView component on Android.
// It might a be ScrollView, NestedScrollView (different inheritance hierarchies) or any other
// ViewGroup that implements appropriate scrolling interfaces.
fun registerScrollView(maker: ScrollViewMarker, scrollView: ViewGroup)
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The parameter name maker should be marker to correctly identify the ScrollViewMarker instance being registered.

Suggested change
fun registerScrollView(maker: ScrollViewMarker, scrollView: ViewGroup)
fun registerScrollView(marker: ScrollViewMarker, scrollView: ViewGroup)

Copilot uses AI. Check for mistakes.
Comment on lines +2 to +7
import TestSvmDetectsScrollView from './test-svm-configures-scroll-view';

const ScrollViewMarkerScenarioGroup: ScenarioGroup = {
name: 'ScrollViewMarker scenarios',
details: 'Scenarios related to ScrollViewMarker component',
scenarios: [TestSvmDetectsScrollView],
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The imported constant name TestSvmDetectsScrollView does not match the actual scenario's name 'Basic functionality' or key 'test-svm-configures-scroll-view'. The import name suggests it's about detecting the scroll view, while the actual scenario key indicates it configures the scroll view. Consider renaming the import to match the scenario, such as TestSvmConfiguresScrollView.

Suggested change
import TestSvmDetectsScrollView from './test-svm-configures-scroll-view';
const ScrollViewMarkerScenarioGroup: ScenarioGroup = {
name: 'ScrollViewMarker scenarios',
details: 'Scenarios related to ScrollViewMarker component',
scenarios: [TestSvmDetectsScrollView],
import TestSvmConfiguresScrollView from './test-svm-configures-scroll-view';
const ScrollViewMarkerScenarioGroup: ScenarioGroup = {
name: 'ScrollViewMarker scenarios',
details: 'Scenarios related to ScrollViewMarker component',
scenarios: [TestSvmConfiguresScrollView],

Copilot uses AI. Check for mistakes.
Comment on lines +195 to +204
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[super mountChildComponentView:childComponentView index:index];
}

- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[super unmountChildComponentView:childComponentView index:index];
}

Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

These method overrides are empty and only call the superclass implementation without adding any additional logic. Consider removing them unless they're intended as placeholders for future implementation. Empty overrides add unnecessary code and can be confusing to maintainers.

Suggested change
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[super mountChildComponentView:childComponentView index:index];
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[super unmountChildComponentView:childComponentView index:index];
}

Copilot uses AI. Check for mistakes.
// want to handle such case.
if (_needsEdgeEffectUpdate) {
_needsEdgeEffectUpdate = NO;
[self configureScrollView:[self findScrollView]];
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The configureScrollView: method expects a nonnull UIScrollView parameter, but it's called with the result of findScrollView which can return nil (line 60-69). This creates a potential crash if findScrollView returns nil. Consider either making the parameter nullable and adding a nil check, or ensuring that findScrollView never returns nil before calling this method.

Suggested change
[self configureScrollView:[self findScrollView]];
UIScrollView *scrollView = [self findScrollView];
if (scrollView != nil) {
[self configureScrollView:scrollView];
}

Copilot uses AI. Check for mistakes.
export { default as Split } from '../components/gamma/split';
export { default as SafeAreaView } from '../components/safe-area/SafeAreaView';

export { ScrollViewMarker } from '../components/gamma/scroll-view-marker/ScrollViewMarker';
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

ScrollViewMarker uses a named export while other experimental components like Stack and SafeAreaView use default exports. Consider changing this to a default export for consistency with the existing component export pattern in the experimental API.

Suggested change
export { ScrollViewMarker } from '../components/gamma/scroll-view-marker/ScrollViewMarker';
export { default as ScrollViewMarker } from '../components/gamma/scroll-view-marker/ScrollViewMarker';

Copilot uses AI. Check for mistakes.
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