feat: add ScrollViewMarker experimental component#3674
Conversation
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.
ScrollViewMarker experimental componentScrollViewMarker experimental component
| - (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]; | ||
| } |
There was a problem hiding this comment.
I've left these for easier debugging.
There was a problem hiding this comment.
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
ScrollViewMarkerexperimental component with TypeScript and native (iOS/Android) implementations - Implemented scroll edge effect configuration support for iOS 26+
- Introduced
RNSScrollViewSeekingprotocol 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) |
There was a problem hiding this comment.
The parameter name maker should be marker to correctly identify the ScrollViewMarker instance being registered.
| fun registerScrollView(maker: ScrollViewMarker, scrollView: ViewGroup) | |
| fun registerScrollView(marker: ScrollViewMarker, scrollView: ViewGroup) |
| import TestSvmDetectsScrollView from './test-svm-configures-scroll-view'; | ||
|
|
||
| const ScrollViewMarkerScenarioGroup: ScenarioGroup = { | ||
| name: 'ScrollViewMarker scenarios', | ||
| details: 'Scenarios related to ScrollViewMarker component', | ||
| scenarios: [TestSvmDetectsScrollView], |
There was a problem hiding this comment.
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.
| 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], |
| - (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]; | ||
| } | ||
|
|
There was a problem hiding this comment.
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.
| - (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]; | |
| } |
| // want to handle such case. | ||
| if (_needsEdgeEffectUpdate) { | ||
| _needsEdgeEffectUpdate = NO; | ||
| [self configureScrollView:[self findScrollView]]; |
There was a problem hiding this comment.
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.
| [self configureScrollView:[self findScrollView]]; | |
| UIScrollView *scrollView = [self findScrollView]; | |
| if (scrollView != nil) { | |
| [self configureScrollView:scrollView]; | |
| } |
| 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'; |
There was a problem hiding this comment.
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.
| export { ScrollViewMarker } from '../components/gamma/scroll-view-marker/ScrollViewMarker'; | |
| export { default as ScrollViewMarker } from '../components/gamma/scroll-view-marker/ScrollViewMarker'; |
Description
This PR adds a PoC of
ScrollViewMarkerexperimental native component.It is introduced to handle two responsibilities:
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.Detection of
ScrollViewin native view hierarchyCurrently, both we & UIKit employ very simple "first descendant chain" heuristics
to detect the presence of
ScrollViewin 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
ScrollViewin our component, so thisis 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
ScrollViewsa user want to configure. There can be only a single "content
ScrollView".Atm. I think that reasonable approach is to have non-required
isContentScrollViewproperty with
trueas default value, and if there are multipleScrollViewinstances,we can throw an error asking to set this property to
falsefor all, but theactual "content
ScrollView". We should decide on default value ofisContentScrollViewbased 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.
RNSScrollViewSeekingprotocol is introduced. Other component can implement it to benotified when
ScrollViewMarkeris attached to view hierarchy. Currently it is notused 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.
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 usesStackContainerto 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