Skip to content

Commit f057792

Browse files
Calvin Liufacebook-github-bot
authored andcommitted
Hide misplaced iOS 26 scroll edge effects on inverted lists
Summary: iOS 26 adds scroll edge effects, which UIKit positions in a scroll view's own coordinate space. Inverted virtualized lists are implemented with a `scaleY(-1)` transform on the scroll view, and UIKit does not account for it, so neither effect lands where it belongs: - Against a navigation bar, `topEdgeEffect` darkens the lower half of the screen instead of the content passing under the bar. - Against a bottom bar, `bottomEdgeEffect` dims the entire screen. With both bars present the two stack, leaving content close to unreadable. This reproduces with a plain `UIScrollView` carrying the same transform, with no React Native involved, so the underlying defect is in UIKit rather than here. What can be done from this side is stop the misplaced effects from rendering, so `RCTEnhancedScrollView` now hides both while the scroll view is inverted. Nothing visible is lost by doing so. An inverted list never gets a usable edge effect at any scroll position; the only thing the effects produce while mirrored is the misplaced darkening. Giving the bars correct effects is not possible from here, because UIKit has no notion of the transform. Reported as react-native#54181, migrated from software-mansion/react-native-screens#3293. Changelog: [iOS][Fixed] - Hide the misplaced iOS 26 scroll edge effects on inverted ScrollViews Differential Revision: D120641422
1 parent 972860a commit f057792

6 files changed

Lines changed: 33 additions & 0 deletions

File tree

‎packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ NS_ASSUME_NONNULL_BEGIN
5252
@property (nonatomic, assign) BOOL snapToEnd;
5353
@property (nonatomic, copy) NSArray<NSNumber *> *snapToOffsets;
5454

55+
/*
56+
* Whether this scroll view is mirrored by a `scaleY(-1)` transform, as inverted virtualized lists
57+
* are. UIKit does not account for that transform when it positions iOS 26 scroll edge effects, so
58+
* they have to be compensated for here.
59+
*/
60+
@property (nonatomic, assign) BOOL inverted;
61+
5562
/*
5663
* Makes `setContentOffset:` method no-op when given `block` is executed.
5764
* The block is being executed synchronously.

‎packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,26 @@ - (instancetype)initWithFrame:(CGRect)frame
5151
return self;
5252
}
5353

54+
- (void)setInverted:(BOOL)inverted
55+
{
56+
if (_inverted == inverted) {
57+
return;
58+
}
59+
_inverted = inverted;
60+
61+
if (@available(iOS 26.0, *)) {
62+
// UIKit positions the scroll edge effects in this scroll view's own coordinate space, and the
63+
// `scaleY(-1)` mirror turns that space upside down, so neither effect lands where it belongs.
64+
// Against a navigation bar, `topEdgeEffect` darkens the lower half of the screen rather than
65+
// the content passing under the bar; against a bottom bar, `bottomEdgeEffect` dims the whole
66+
// screen. Neither produces anything useful at any scroll position while mirrored, so both are
67+
// hidden. The bars cannot be given correct effects from here, because UIKit has no notion of
68+
// the transform.
69+
self.topEdgeEffect.hidden = inverted;
70+
self.bottomEdgeEffect.hidden = inverted;
71+
}
72+
}
73+
5474
- (void)preserveContentOffsetWithBlock:(void (^)())block
5575
{
5676
if (!block) {

‎packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,9 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
455455
}
456456

457457
[super updateProps:props oldProps:oldProps];
458+
459+
// Must run after `super updateProps:` so it is based on the latest calculated view.
460+
((RCTEnhancedScrollView *)_scrollView).inverted = [self isInverted];
458461
}
459462

460463
- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState

‎scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,6 +1035,7 @@ interface RCTDivisionAnimatedNode : public RCTValueAnimatedNode {
10351035
interface RCTEnhancedScrollView : public UIScrollView {
10361036
public @property (assign) BOOL centerContent;
10371037
public @property (assign) BOOL disableIntervalMomentum;
1038+
public @property (assign) BOOL inverted;
10381039
public @property (assign) BOOL pinchGestureEnabled;
10391040
public @property (assign) BOOL snapToEnd;
10401041
public @property (assign) BOOL snapToStart;

‎scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,6 +1035,7 @@ interface RCTDivisionAnimatedNode : public RCTValueAnimatedNode {
10351035
interface RCTEnhancedScrollView : public UIScrollView {
10361036
public @property (assign) BOOL centerContent;
10371037
public @property (assign) BOOL disableIntervalMomentum;
1038+
public @property (assign) BOOL inverted;
10381039
public @property (assign) BOOL pinchGestureEnabled;
10391040
public @property (assign) BOOL snapToEnd;
10401041
public @property (assign) BOOL snapToStart;

‎scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,6 +1035,7 @@ interface RCTDivisionAnimatedNode : public RCTValueAnimatedNode {
10351035
interface RCTEnhancedScrollView : public UIScrollView {
10361036
public @property (assign) BOOL centerContent;
10371037
public @property (assign) BOOL disableIntervalMomentum;
1038+
public @property (assign) BOOL inverted;
10381039
public @property (assign) BOOL pinchGestureEnabled;
10391040
public @property (assign) BOOL snapToEnd;
10401041
public @property (assign) BOOL snapToStart;

0 commit comments

Comments
 (0)