@onekeyfe/react-native-pager-view 3.0.178 → 3.0.179-alpha.2026101501

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  #import "RNCCollapsiblePagerViewComponentView.h"
2
+ #import "RNCPagerViewComponentView.h"
2
3
  #import "RNCNativeScrollerComponentView.h"
3
4
  #import "RNCCollapsiblePagerReleasedStatePolicy.h"
4
5
 
@@ -194,6 +195,23 @@ static BOOL RNCIsEnabledHorizontalScrollOwner(UIScrollView *scroll)
194
195
  scroll.contentSize.height <= CGRectGetHeight(scroll.bounds) + 0.5;
195
196
  }
196
197
 
198
+ static UIScrollView *RNCNestedOuterReactPagerScrollView(UIView *view)
199
+ {
200
+ Class outerPagerClass = NSClassFromString(@"RNCPagerViewComponentView");
201
+ if (outerPagerClass == Nil) return nil;
202
+ UIScrollView *pagingAncestor = nil;
203
+ for (UIView *ancestor = view.superview; ancestor != nil; ancestor = ancestor.superview) {
204
+ if ([ancestor isKindOfClass:UIScrollView.class] &&
205
+ ((UIScrollView *)ancestor).pagingEnabled) {
206
+ pagingAncestor = (UIScrollView *)ancestor;
207
+ }
208
+ if ([ancestor isKindOfClass:outerPagerClass]) {
209
+ return pagingAncestor.panGestureRecognizer.enabled ? pagingAncestor : nil;
210
+ }
211
+ }
212
+ return nil;
213
+ }
214
+
197
215
  static void RNCGuardNestedHorizontalPans(UIView *view, UIGestureRecognizer *guard)
198
216
  {
199
217
  if ([view isKindOfClass:UIScrollView.class]) {
@@ -219,6 +237,7 @@ static void RNCCollapsiblePagerLog(NSString *message)
219
237
  logFunction(logClass, selector, @"CollapsiblePager", message);
220
238
  }
221
239
 
240
+
222
241
  static BOOL RNCGetColorComponents(UIColor *color, UITraitCollection *traits,
223
242
  CGFloat *red, CGFloat *green, CGFloat *blue, CGFloat *alpha)
224
243
  {
@@ -1042,7 +1061,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1042
1061
  CGFloat maximum = MAX(0, inner.contentSize.width - CGRectGetWidth(inner.bounds));
1043
1062
  BOOL canScroll = maximum > 0 && (intent.x < 0
1044
1063
  ? inner.contentOffset.x < maximum - 0.5 : inner.contentOffset.x > 0.5);
1045
- if (canScroll) return NO;
1064
+ if (canScroll) {
1065
+ return NO;
1066
+ }
1046
1067
  }
1047
1068
  }
1048
1069
  if (gestureRecognizer == self.panGestureRecognizer && self.excludedHeaderView != nil) {
@@ -1052,7 +1073,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1052
1073
  return NO;
1053
1074
  }
1054
1075
  }
1055
- return [super gestureRecognizerShouldBegin:gestureRecognizer];
1076
+ BOOL result = [super gestureRecognizerShouldBegin:gestureRecognizer];
1077
+ return result;
1056
1078
  }
1057
1079
 
1058
1080
  @end
@@ -1241,6 +1263,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1241
1263
 
1242
1264
  @interface RNCCollapsiblePagerOuterPagerPanGestureRecognizer : UIPanGestureRecognizer
1243
1265
  @property (nonatomic, strong) NSHashTable<UIGestureRecognizer *> *blockedPagerGestures;
1266
+ @property (nonatomic, weak) UITouch *trackedTouch;
1267
+ @property (nonatomic, assign) BOOL protectsNestedBody;
1244
1268
  @end
1245
1269
 
1246
1270
  @implementation RNCCollapsiblePagerOuterPagerPanGestureRecognizer
@@ -1265,6 +1289,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1265
1289
  return NO;
1266
1290
  }
1267
1291
 
1292
+ - (void)reset
1293
+ {
1294
+ [super reset];
1295
+ self.trackedTouch = nil;
1296
+ self.protectsNestedBody = NO;
1297
+ self.cancelsTouchesInView = YES;
1298
+ }
1299
+
1268
1300
  @end
1269
1301
 
1270
1302
  @interface RNCCollapsiblePagerVerticalPagerGuardGestureRecognizer : UIGestureRecognizer
@@ -1408,7 +1440,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1408
1440
  UIScrollViewDelegate,
1409
1441
  UIGestureRecognizerDelegate,
1410
1442
  RCTMountingTransactionObserving,
1411
- RNCNativeScrollerInsetCoordinator
1443
+ RNCNativeScrollerInsetCoordinator,
1444
+ RNCPagerViewNestedOverlayObserver
1412
1445
  >
1413
1446
  - (void)finishPagerScrollEmittingSelection:(BOOL)emitSelection;
1414
1447
  - (void)completeTransitionOnNextRunLoopForGeneration:(NSUInteger)generation
@@ -1418,6 +1451,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1418
1451
  - (void)liftSharedHeadersForPagerTransition:(NSString *)reason;
1419
1452
  - (void)restoreSharedHeadersToContainer;
1420
1453
  - (void)layoutSharedHeaders;
1454
+ - (void)restorePinnedStickyHeader;
1455
+ - (void)updatePinnedStickyHeader;
1421
1456
  - (UIScrollView *)currentVerticalScrollViewForParentPager;
1422
1457
  - (UIScrollView *)verticalScrollViewForPageAtIndex:(NSInteger)pageIndex;
1423
1458
  - (UIScrollView *)findExplicitNativeScrollerInView:(UIView *)view;
@@ -1429,19 +1464,32 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1429
1464
  - (void)detachSharedHeaderPressCancellationGesture;
1430
1465
  - (void)updateRefreshControlForScrollView:(UIScrollView *)scrollView;
1431
1466
  - (void)releaseRefreshControlForScrollView:(UIScrollView *)scrollView;
1467
+ - (void)cancelTopInsetReboundCheck;
1468
+ - (void)scheduleTopInsetReboundCheck;
1469
+ - (void)syncScrollEdgeAppearance;
1470
+ - (void)updateScrollEdgeContentMasks;
1471
+ - (void)restoreScrollEdgeContentMasks;
1432
1472
  - (void)sharedHeaderPressCancellationGestureChanged:(UIPanGestureRecognizer *)recognizer;
1433
1473
  - (void)outerPagerGestureChanged:(UIPanGestureRecognizer *)recognizer;
1434
1474
  - (void)verticalPagerGuardGestureChanged:(UIPanGestureRecognizer *)recognizer;
1435
1475
  - (void)connectVerticalPagerGuard;
1436
1476
  - (void)observedListPanChanged:(UIPanGestureRecognizer *)recognizer;
1477
+ - (BOOL)resolveExternalTopOverlay;
1478
+ - (void)registerExternalOuterPager;
1479
+ - (CGFloat)effectiveTopOverlayHeight;
1480
+ - (BOOL)hasFixedTopOverlay;
1437
1481
  @end
1438
1482
 
1439
1483
  @implementation RNCCollapsiblePagerViewComponentView {
1440
- #if DEBUG
1441
- NSUInteger _offsetDiagSequence;
1442
- #endif
1443
1484
  UIView *_containerView;
1444
1485
  RNCCollapsiblePagerSharedHeaderView *_sharedHeaderHostView;
1486
+ UIView *_pinnedStickyHostView;
1487
+ BOOL _stickyHeaderIsPinned;
1488
+ UIView *_sharedHeaderMaskView;
1489
+ UIView *_originalSharedHeaderMaskView;
1490
+ NSMapTable<UIView *, id> *_originalPageMasks;
1491
+ NSMapTable<UIView *, UIView *> *_ownedPageMasks;
1492
+ CGFloat _pageMaskBoundary;
1445
1493
  UIPageViewController *_pageViewController;
1446
1494
  UIScrollView *_pageViewControllerScrollView;
1447
1495
  UIScrollView *_pagerScrollView;
@@ -1449,6 +1497,16 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1449
1497
  NSMutableArray<UIViewController *> *_pageControllers;
1450
1498
  UIView *_headerView;
1451
1499
  UIView *_stickyHeaderView;
1500
+ UIView *_topOverlayView;
1501
+ UIView *_topOverlayHostView;
1502
+ UIScrollEdgeElementContainerInteraction *_topEdgeInteraction;
1503
+ UIScrollEdgeElementContainerInteraction *_stickyEdgeInteraction;
1504
+ UILabel *_topEdgeMarker;
1505
+ UILabel *_stickyEdgeMarker;
1506
+ __weak UIView *_topEdgeInteractionHost;
1507
+ __weak UIView *_stickyEdgeInteractionHost;
1508
+ __weak UIScrollView *_topEdgeScrollView;
1509
+ UIScrollEdgeEffectStyle *_originalTopEdgeStyle;
1452
1510
  RNCCollapsiblePagerNativeTabBarView *_nativeTabBarView;
1453
1511
  RNCCollapsiblePagerNativeSubHeaderView *_nativeSubHeaderView;
1454
1512
  NSInteger _currentIndex;
@@ -1456,6 +1514,22 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1456
1514
  NSInteger _pendingInitialPage;
1457
1515
  NSInteger _headerHeight;
1458
1516
  NSInteger _stickyHeaderHeight;
1517
+ CGFloat _topOverlayHeight;
1518
+ NSInteger _externalTopOverlayTag;
1519
+ __weak UIView *_externalTopOverlayView;
1520
+ CGFloat _externalTopOverlayHeight;
1521
+ __weak RNCPagerViewComponentView *_externalOuterPager;
1522
+ BOOL _externalOuterPageActive;
1523
+ BOOL _externalOuterTransitioning;
1524
+ BOOL _topScrollEdgeHard;
1525
+ NSString *_topOverlayColorScheme;
1526
+ BOOL _pendingTopOverlayAppearanceCommit;
1527
+ BOOL _applyingTopOverlayInsetChange;
1528
+ BOOL _correctingTopInsetRebound;
1529
+ __weak UIScrollView *_pendingTopInsetReboundScrollView;
1530
+ NSString *_pendingTopInsetReboundPageKey;
1531
+ NSUInteger _pendingTopInsetReboundGeneration;
1532
+ NSTimer *_topInsetReboundTimer;
1459
1533
  CGFloat _nativeTabBarHeight;
1460
1534
  CGFloat _nativeSubHeaderHeight;
1461
1535
  CGFloat _headerOffset;
@@ -1475,6 +1549,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1475
1549
  NSInteger _pendingGoToIndex;
1476
1550
  BOOL _pendingGoToAnimated;
1477
1551
  BOOL _needsSlotRebuild;
1552
+ BOOL _deferTopEdgeSlotRebuild;
1478
1553
  BOOL _hasAppliedInitialPage;
1479
1554
  BOOL _needsPropsReapply;
1480
1555
  NSString *_layoutDirection;
@@ -1512,9 +1587,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1512
1587
  NSUInteger _observedPageGeneration;
1513
1588
  NSDictionary<NSString *, NSNumber *> *_transitionPageOffsets;
1514
1589
  NSMutableIndexSet *_transitionCompensatedPages;
1515
- #if DEBUG
1516
- NSInteger _transitionDiagBucket;
1517
- #endif
1518
1590
  NSInteger _transitionSourceIndex;
1519
1591
  BOOL _syntheticPageRestore;
1520
1592
  BOOL _headerSettledAtTop;
@@ -1580,6 +1652,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1580
1652
  _originalAlwaysBounceVertical = [NSMapTable weakToStrongObjectsMapTable];
1581
1653
  _originalDirectionalLockEnabled = [NSMapTable weakToStrongObjectsMapTable];
1582
1654
  _originalInsetAdjustmentBehavior = [NSMapTable weakToStrongObjectsMapTable];
1655
+ _originalPageMasks = [NSMapTable strongToStrongObjectsMapTable];
1656
+ _ownedPageMasks = [NSMapTable strongToStrongObjectsMapTable];
1583
1657
  _pageKeys = @[];
1584
1658
  _retainedPages = @"[]";
1585
1659
  _currentIndex = 0;
@@ -1607,6 +1681,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1607
1681
  _sharedHeaderHostView = [RNCCollapsiblePagerSharedHeaderView new];
1608
1682
  _sharedHeaderHostView.clipsToBounds = NO;
1609
1683
  [_containerView addSubview:_sharedHeaderHostView];
1684
+ _pinnedStickyHostView = [UIView new];
1685
+ _pinnedStickyHostView.hidden = YES;
1686
+ [_containerView addSubview:_pinnedStickyHostView];
1687
+ _topOverlayHostView = [UIView new];
1688
+ _topOverlayHostView.hidden = YES;
1689
+ [_containerView addSubview:_topOverlayHostView];
1610
1690
 
1611
1691
  _verticalPagerGesture =
1612
1692
  [[RNCCollapsiblePagerVerticalPagerGuardGestureRecognizer alloc]
@@ -1664,14 +1744,39 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1664
1744
  viewProps.pointerEvents == PointerEventsMode::BoxOnly) {
1665
1745
  return [super hitTest:point withEvent:event];
1666
1746
  }
1747
+ if (_topOverlayView != nil) {
1748
+ CGPoint overlayPoint = [_topOverlayView convertPoint:point fromView:self];
1749
+ UIView *overlayHitView = [_topOverlayView hitTest:overlayPoint withEvent:event];
1750
+ if (overlayHitView != nil) return overlayHitView;
1751
+ }
1752
+ if (_stickyHeaderIsPinned) {
1753
+ CGPoint stickyPoint = [_pinnedStickyHostView convertPoint:point fromView:self];
1754
+ UIView *stickyHitView = [_pinnedStickyHostView hitTest:stickyPoint withEvent:event];
1755
+ if (stickyHitView != nil) return stickyHitView;
1756
+ }
1757
+ CGPoint headerPoint = [_sharedHeaderHostView convertPoint:point fromView:self];
1758
+ BOOL sharedHeaderPointIsVisible =
1759
+ _sharedHeaderHostView.maskView != _sharedHeaderMaskView ||
1760
+ _sharedHeaderMaskView == nil ||
1761
+ CGRectContainsPoint(_sharedHeaderMaskView.frame, headerPoint);
1667
1762
  if (_nativeSmoothHeaderScrollEnabled && !_isBeingRecycled &&
1668
- _sharedHeaderHostView.superview != _containerView) {
1669
- CGPoint headerPoint = [_sharedHeaderHostView convertPoint:point fromView:self];
1763
+ _sharedHeaderHostView.superview != _containerView &&
1764
+ sharedHeaderPointIsVisible) {
1670
1765
  UIView *headerHitView =
1671
1766
  [_sharedHeaderHostView hitTest:headerPoint withEvent:event];
1672
1767
  if (headerHitView != nil) return headerHitView;
1673
1768
  }
1674
- return [super hitTest:point withEvent:event];
1769
+ UIView *hitView = [super hitTest:point withEvent:event];
1770
+ if (hitView != nil && !sharedHeaderPointIsVisible &&
1771
+ (hitView == _sharedHeaderHostView ||
1772
+ [hitView isDescendantOfView:_sharedHeaderHostView])) return nil;
1773
+ if (hitView != nil && _ownedPageMasks.count > 0 &&
1774
+ [_containerView convertPoint:point fromView:self].y < _pageMaskBoundary) {
1775
+ for (UIView *page in _ownedPageMasks.keyEnumerator.allObjects) {
1776
+ if ([hitView isDescendantOfView:page]) return nil;
1777
+ }
1778
+ }
1779
+ return hitView;
1675
1780
  }
1676
1781
 
1677
1782
  - (void)willMoveToSuperview:(UIView *)newSuperview
@@ -1686,11 +1791,18 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1686
1791
  - (void)didMoveToWindow
1687
1792
  {
1688
1793
  [super didMoveToWindow];
1794
+ [self registerExternalOuterPager];
1795
+ BOOL externalHeightChanged = [self resolveExternalTopOverlay];
1796
+ if (self.window == nil) [self restoreScrollEdgeContentMasks];
1797
+ [self syncScrollEdgeAppearance];
1798
+ [self emitTopScrollEdgeCapability];
1799
+ [self updateTopScrollEdgeForScrollView:_observedScrollView];
1689
1800
  [self updateSharedHeaderPressCancellationGesture];
1690
1801
  [self updateHeaderSettledStateForceBusy:NO];
1691
1802
  for (UIScrollView *scrollView in _originalInsets.keyEnumerator) {
1692
- [self updateNativeScrollerViewport:scrollView stickyHeight:self.window == nil ? 0 : _stickyHeaderHeight];
1803
+ [self updateNativeScrollerViewport:scrollView stickyHeight:self.window == nil ? 0 : _stickyHeaderHeight + self.effectiveTopOverlayHeight];
1693
1804
  }
1805
+ if (externalHeightChanged) [self reapplyInsetsToObservedScrollView];
1694
1806
  // OneKey patch: removing a decelerating page from its window can suppress
1695
1807
  // UIKit's final scroll callback. Restore the last acknowledged page on reattach.
1696
1808
  if (self.window != nil && _isPagerDragging &&
@@ -1712,8 +1824,319 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1712
1824
  }
1713
1825
  }
1714
1826
 
1827
+ - (CGFloat)effectiveTopOverlayHeight
1828
+ {
1829
+ return _topOverlayView != nil ? _topOverlayHeight : _externalTopOverlayHeight;
1830
+ }
1831
+
1832
+ - (BOOL)hasFixedTopOverlay
1833
+ {
1834
+ return _topOverlayView != nil ||
1835
+ (_externalTopOverlayTag > 0 && _externalTopOverlayHeight > 0);
1836
+ }
1837
+
1838
+ - (void)registerExternalOuterPager
1839
+ {
1840
+ RNCPagerViewComponentView *outer = nil;
1841
+ if (_externalTopOverlayTag > 0 && self.window != nil && !_isBeingRecycled) {
1842
+ for (UIView *ancestor = self.superview; ancestor != nil; ancestor = ancestor.superview) {
1843
+ if ([ancestor isKindOfClass:RNCPagerViewComponentView.class]) {
1844
+ outer = (RNCPagerViewComponentView *)ancestor;
1845
+ break;
1846
+ }
1847
+ }
1848
+ }
1849
+ if (_externalOuterPager == outer) return;
1850
+ [_externalOuterPager removeNestedOverlayObserver:self];
1851
+ _externalOuterPager = outer;
1852
+ _externalOuterPageActive = NO;
1853
+ _externalOuterTransitioning = NO;
1854
+ [outer addNestedOverlayObserver:self];
1855
+ }
1856
+
1857
+ - (BOOL)resolveExternalTopOverlay
1858
+ {
1859
+ UIView *nextView = nil;
1860
+ CGFloat nextHeight = 0;
1861
+ if (_externalTopOverlayTag > 0 && _topOverlayView == nil &&
1862
+ self.window != nil && !_isBeingRecycled) {
1863
+ for (UIView *ancestor = self.superview; ancestor != nil; ancestor = ancestor.superview) {
1864
+ UIView *candidate = [ancestor viewWithTag:_externalTopOverlayTag];
1865
+ if (candidate == nil || candidate == self ||
1866
+ [candidate isDescendantOfView:self] || candidate.window != self.window) continue;
1867
+ nextView = candidate;
1868
+ break;
1869
+ }
1870
+ if (nextView != nil && CGRectGetHeight(nextView.bounds) > 0) {
1871
+ nextHeight = MAX(0, CGRectGetMaxY([nextView convertRect:nextView.bounds
1872
+ toView:_containerView]));
1873
+ }
1874
+ }
1875
+ if (nextView == nil && _externalOuterTransitioning &&
1876
+ _externalTopOverlayTag > 0 && self.window != nil) {
1877
+ nextHeight = _externalTopOverlayHeight;
1878
+ }
1879
+ BOOL changed = _externalTopOverlayView != nextView ||
1880
+ fabs(_externalTopOverlayHeight - nextHeight) > 0.5;
1881
+ _externalTopOverlayView = nextView;
1882
+ _externalTopOverlayHeight = nextHeight;
1883
+ _topOverlayHostView.hidden = !self.hasFixedTopOverlay;
1884
+ _topOverlayHostView.userInteractionEnabled = _topOverlayView != nil;
1885
+ return changed;
1886
+ }
1887
+
1888
+ - (void)outerPagerDidUpdateOverlayOwner:(BOOL)active transitioning:(BOOL)transitioning
1889
+ {
1890
+ if (_isBeingRecycled || _externalOuterPager == nil || _externalTopOverlayTag <= 0) return;
1891
+ if (_externalOuterPageActive == active && _externalOuterTransitioning == transitioning) return;
1892
+ BOOL wasTransitioning = _externalOuterTransitioning;
1893
+ _externalOuterPageActive = active;
1894
+ _externalOuterTransitioning = transitioning;
1895
+ if (transitioning && !wasTransitioning) {
1896
+ [self updateTopScrollEdgeForScrollView:nil];
1897
+ [self liftSharedHeadersForPagerTransition:@"outer"];
1898
+ } else if (!transitioning && wasTransitioning &&
1899
+ !_transitioning && !_isPagerDragging && active) {
1900
+ [self attachSharedHeadersToScrollView:_observedScrollView];
1901
+ }
1902
+ [self updateTopScrollEdgeForScrollView:transitioning ? nil : _observedScrollView];
1903
+ [self updateScrollEdgeContentMasks];
1904
+ [self setNeedsLayout];
1905
+ }
1906
+
1907
+ - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
1908
+ {
1909
+ [super traitCollectionDidChange:previousTraitCollection];
1910
+ if (previousTraitCollection.userInterfaceStyle != self.traitCollection.userInterfaceStyle) {
1911
+ [self syncScrollEdgeAppearance];
1912
+ }
1913
+ }
1914
+
1915
+ - (void)syncScrollEdgeAppearance
1916
+ {
1917
+ BOOL active = _topScrollEdgeHard &&
1918
+ (self.hasFixedTopOverlay || _stickyHeaderView != nil) && self.window != nil &&
1919
+ !_isBeingRecycled;
1920
+ UIUserInterfaceStyle style = UIUserInterfaceStyleUnspecified;
1921
+ if (active) {
1922
+ if ([_topOverlayColorScheme isEqualToString:@"dark"]) {
1923
+ style = UIUserInterfaceStyleDark;
1924
+ } else if ([_topOverlayColorScheme isEqualToString:@"light"]) {
1925
+ style = UIUserInterfaceStyleLight;
1926
+ } else {
1927
+ style = self.traitCollection.userInterfaceStyle;
1928
+ }
1929
+ }
1930
+ if (_topOverlayHostView.overrideUserInterfaceStyle == style) return;
1931
+ _topOverlayHostView.overrideUserInterfaceStyle = style;
1932
+ _pinnedStickyHostView.overrideUserInterfaceStyle = style;
1933
+ }
1934
+
1935
+ - (void)restoreScrollEdgeContentMasks
1936
+ {
1937
+ if (_sharedHeaderHostView.maskView == _sharedHeaderMaskView) {
1938
+ _sharedHeaderHostView.maskView = _originalSharedHeaderMaskView;
1939
+ }
1940
+ _sharedHeaderMaskView = nil;
1941
+ _originalSharedHeaderMaskView = nil;
1942
+ for (UIView *page in _ownedPageMasks.keyEnumerator.allObjects) {
1943
+ if (page.maskView == [_ownedPageMasks objectForKey:page]) {
1944
+ id original = [_originalPageMasks objectForKey:page];
1945
+ page.maskView = original == NSNull.null ? nil : original;
1946
+ }
1947
+ }
1948
+ [_ownedPageMasks removeAllObjects];
1949
+ [_originalPageMasks removeAllObjects];
1950
+ _pageMaskBoundary = 0;
1951
+ }
1952
+
1953
+ - (void)updateScrollEdgeContentMasks
1954
+ {
1955
+ BOOL enabled = _topScrollEdgeHard &&
1956
+ (self.hasFixedTopOverlay || _stickyHeaderView != nil) &&
1957
+ self.window != nil && !_isBeingRecycled;
1958
+ if (!enabled) {
1959
+ [self restoreScrollEdgeContentMasks];
1960
+ return;
1961
+ }
1962
+ if (self.hasFixedTopOverlay && _sharedHeaderHostView.window == self.window &&
1963
+ CGRectGetHeight(_sharedHeaderHostView.bounds) > 0 &&
1964
+ CGRectGetHeight(_topOverlayHostView.bounds) > 0) {
1965
+ if (_sharedHeaderMaskView == nil) {
1966
+ _originalSharedHeaderMaskView = _sharedHeaderHostView.maskView;
1967
+ _sharedHeaderMaskView = [UIView new];
1968
+ _sharedHeaderMaskView.backgroundColor = UIColor.blackColor;
1969
+ _sharedHeaderHostView.maskView = _sharedHeaderMaskView;
1970
+ }
1971
+ CGFloat topBottom = CGRectGetMaxY([_topOverlayHostView
1972
+ convertRect:_topOverlayHostView.bounds toView:_containerView]);
1973
+ CGFloat localTop = [_containerView convertPoint:CGPointMake(0, topBottom)
1974
+ toView:_sharedHeaderHostView].y;
1975
+ _sharedHeaderMaskView.frame = CGRectMake(0, localTop,
1976
+ CGRectGetWidth(_sharedHeaderHostView.bounds),
1977
+ MAX(0, CGRectGetHeight(_sharedHeaderHostView.bounds) - localTop));
1978
+ } else if (_sharedHeaderMaskView != nil) {
1979
+ if (_sharedHeaderHostView.maskView == _sharedHeaderMaskView) {
1980
+ _sharedHeaderHostView.maskView = _originalSharedHeaderMaskView;
1981
+ }
1982
+ _sharedHeaderMaskView = nil;
1983
+ _originalSharedHeaderMaskView = nil;
1984
+ }
1985
+
1986
+ BOOL transitioning = _transitioning || _isPagerDragging || _externalOuterTransitioning ||
1987
+ _sharedHeadersLiftedForPagerTransition;
1988
+ CGFloat boundary = CGRectGetMaxY([_topOverlayHostView
1989
+ convertRect:_topOverlayHostView.bounds toView:_containerView]);
1990
+ UIView *sticky = _stickyHeaderIsPinned ? _pinnedStickyHostView : _stickyHeaderView;
1991
+ if (sticky != nil && !sticky.hidden && sticky.window == self.window &&
1992
+ CGRectGetHeight(sticky.bounds) > 0) {
1993
+ boundary = MAX(boundary, CGRectGetMaxY([sticky
1994
+ convertRect:sticky.bounds toView:_containerView]));
1995
+ }
1996
+ _pageMaskBoundary = boundary;
1997
+ NSMutableSet<UIView *> *visiblePages = [NSMutableSet new];
1998
+ if (transitioning) {
1999
+ for (UIViewController *controller in _pageControllers) {
2000
+ UIView *page = controller.view;
2001
+ if (page.window != self.window ||
2002
+ !CGRectIntersectsRect([page convertRect:page.bounds toView:_containerView],
2003
+ _containerView.bounds)) continue;
2004
+ [visiblePages addObject:page];
2005
+ UIView *mask = [_ownedPageMasks objectForKey:page];
2006
+ if (mask == nil) {
2007
+ [_originalPageMasks setObject:page.maskView ?: NSNull.null forKey:page];
2008
+ mask = [UIView new];
2009
+ mask.backgroundColor = UIColor.blackColor;
2010
+ [_ownedPageMasks setObject:mask forKey:page];
2011
+ page.maskView = mask;
2012
+ }
2013
+ CGFloat localTop = [_containerView convertPoint:CGPointMake(0, boundary)
2014
+ toView:page].y;
2015
+ mask.frame = CGRectMake(0, localTop, CGRectGetWidth(page.bounds),
2016
+ MAX(0, CGRectGetHeight(page.bounds) - localTop));
2017
+ }
2018
+ }
2019
+ for (UIView *page in _ownedPageMasks.keyEnumerator.allObjects) {
2020
+ if ([visiblePages containsObject:page]) continue;
2021
+ UIView *mask = [_ownedPageMasks objectForKey:page];
2022
+ if (page.maskView == mask) {
2023
+ id original = [_originalPageMasks objectForKey:page];
2024
+ page.maskView = original == NSNull.null ? nil : original;
2025
+ }
2026
+ [_ownedPageMasks removeObjectForKey:page];
2027
+ [_originalPageMasks removeObjectForKey:page];
2028
+ }
2029
+ }
2030
+
2031
+ - (void)updateEventEmitter:(const EventEmitter::Shared &)eventEmitter
2032
+ {
2033
+ [super updateEventEmitter:eventEmitter];
2034
+ [self emitTopScrollEdgeCapability];
2035
+ }
2036
+
2037
+ - (void)emitTopScrollEdgeCapability
2038
+ {
2039
+ if (self.window == nil || _isBeingRecycled) return;
2040
+ const auto emitter = [self eventEmitter];
2041
+ if (!emitter) return;
2042
+ BOOL available = NO;
2043
+ if (@available(iOS 26.0, *)) available = YES;
2044
+ emitter->onScrollEdgeCapability({.available = (bool)available});
2045
+ }
2046
+
2047
+ - (void)updateTopScrollEdgeForScrollView:(UIScrollView *)scrollView
2048
+ {
2049
+ [self syncScrollEdgeAppearance];
2050
+ if (@available(iOS 26.0, *)) {
2051
+ if (_pendingTopOverlayAppearanceCommit) scrollView = nil;
2052
+ BOOL externalOwnerReady = _externalTopOverlayTag <= 0 ||
2053
+ (_externalOuterPageActive && !_externalOuterTransitioning);
2054
+ BOOL hasOverlay = _topScrollEdgeHard && self.hasFixedTopOverlay &&
2055
+ externalOwnerReady && self.window != nil;
2056
+ BOOL hasSticky = _topScrollEdgeHard && _stickyHeaderView != nil &&
2057
+ externalOwnerReady && self.window != nil;
2058
+ BOOL enabled = (hasOverlay || hasSticky) && scrollView != nil;
2059
+ BOOL topHostChanged = (_topEdgeInteraction != nil) != hasOverlay ||
2060
+ (hasOverlay && _topEdgeInteractionHost != _topOverlayHostView);
2061
+ if (_topEdgeScrollView != nil && (!enabled || _topEdgeScrollView != scrollView ||
2062
+ topHostChanged)) {
2063
+ [_topEdgeInteractionHost removeInteraction:_topEdgeInteraction];
2064
+ _topEdgeInteraction.scrollView = nil;
2065
+ [_stickyEdgeInteractionHost removeInteraction:_stickyEdgeInteraction];
2066
+ _stickyEdgeInteraction.scrollView = nil;
2067
+ [_topEdgeMarker removeFromSuperview];
2068
+ [_stickyEdgeMarker removeFromSuperview];
2069
+ _topEdgeScrollView.topEdgeEffect.style = _originalTopEdgeStyle;
2070
+ _topEdgeInteraction = nil;
2071
+ _stickyEdgeInteraction = nil;
2072
+ _topEdgeMarker = nil;
2073
+ _stickyEdgeMarker = nil;
2074
+ _topEdgeInteractionHost = nil;
2075
+ _stickyEdgeInteractionHost = nil;
2076
+ _topEdgeScrollView = nil;
2077
+ _originalTopEdgeStyle = nil;
2078
+ }
2079
+ if (enabled && _topEdgeScrollView == nil) {
2080
+ _originalTopEdgeStyle = scrollView.topEdgeEffect.style;
2081
+ _topEdgeScrollView = scrollView;
2082
+ scrollView.topEdgeEffect.style = UIScrollEdgeEffectStyle.hardStyle;
2083
+ }
2084
+ if (hasOverlay && enabled && _topEdgeInteraction == nil) {
2085
+ // UIKit needs a native element to shape the edge effect around React-drawn content.
2086
+ _topEdgeMarker = [UILabel new];
2087
+ _topEdgeMarker.frame = _topOverlayHostView.bounds;
2088
+ _topEdgeMarker.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
2089
+ _topEdgeMarker.text = @" ";
2090
+ _topEdgeMarker.textColor = UIColor.clearColor;
2091
+ _topEdgeMarker.backgroundColor = UIColor.clearColor;
2092
+ _topEdgeMarker.userInteractionEnabled = NO;
2093
+ _topEdgeMarker.isAccessibilityElement = NO;
2094
+ _topEdgeMarker.accessibilityElementsHidden = YES;
2095
+ [_topOverlayHostView addSubview:_topEdgeMarker];
2096
+ _topEdgeInteraction = [UIScrollEdgeElementContainerInteraction new];
2097
+ _topEdgeInteraction.edge = UIRectEdgeTop;
2098
+ _topEdgeInteraction.scrollView = scrollView;
2099
+ [_topOverlayHostView addInteraction:_topEdgeInteraction];
2100
+ _topEdgeInteractionHost = _topOverlayHostView;
2101
+ }
2102
+ BOOL legacyStickyPinned = !_nativeSmoothHeaderScrollEnabled &&
2103
+ _stickyHeaderView != nil && _headerOffset >= _headerHeight - 0.5;
2104
+ BOOL stickyEnabled = enabled && (_stickyHeaderIsPinned || legacyStickyPinned);
2105
+ UIView *stickyInteractionHost = _stickyHeaderIsPinned
2106
+ ? _pinnedStickyHostView : _stickyHeaderView;
2107
+ if (_stickyEdgeInteractionHost != nil &&
2108
+ (!stickyEnabled || _stickyEdgeInteractionHost != stickyInteractionHost)) {
2109
+ [_stickyEdgeInteractionHost removeInteraction:_stickyEdgeInteraction];
2110
+ _stickyEdgeInteraction.scrollView = nil;
2111
+ [_stickyEdgeMarker removeFromSuperview];
2112
+ _stickyEdgeMarker = nil;
2113
+ _stickyEdgeInteraction = nil;
2114
+ _stickyEdgeInteractionHost = nil;
2115
+ }
2116
+ if (stickyEnabled && _stickyEdgeInteraction == nil) {
2117
+ _stickyEdgeMarker = [UILabel new];
2118
+ _stickyEdgeMarker.frame = stickyInteractionHost.bounds;
2119
+ _stickyEdgeMarker.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
2120
+ _stickyEdgeMarker.text = @" ";
2121
+ _stickyEdgeMarker.textColor = UIColor.clearColor;
2122
+ _stickyEdgeMarker.backgroundColor = UIColor.clearColor;
2123
+ _stickyEdgeMarker.userInteractionEnabled = NO;
2124
+ _stickyEdgeMarker.isAccessibilityElement = NO;
2125
+ _stickyEdgeMarker.accessibilityElementsHidden = YES;
2126
+ [stickyInteractionHost addSubview:_stickyEdgeMarker];
2127
+ _stickyEdgeInteraction = [UIScrollEdgeElementContainerInteraction new];
2128
+ _stickyEdgeInteraction.edge = UIRectEdgeTop;
2129
+ _stickyEdgeInteraction.scrollView = scrollView;
2130
+ [stickyInteractionHost addInteraction:_stickyEdgeInteraction];
2131
+ _stickyEdgeInteractionHost = stickyInteractionHost;
2132
+ }
2133
+ [self updateScrollEdgeContentMasks];
2134
+ }
2135
+ }
2136
+
1715
2137
  - (void)dealloc
1716
2138
  {
2139
+ [self updateTopScrollEdgeForScrollView:nil];
1717
2140
  [self cancelHeaderSettledCheck];
1718
2141
  [[NSNotificationCenter defaultCenter] removeObserver:self name:@"OneKeyNativeListScrollCommandAccepted" object:nil];
1719
2142
  for (UIScrollView *scrollView in _originalInsets.keyEnumerator) {
@@ -1766,6 +2189,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1766
2189
  _hasPendingDirectNativePagerChange = NO;
1767
2190
  if (_directNativePagerEnabled == enabled) return;
1768
2191
 
2192
+ [self restoreScrollEdgeContentMasks];
1769
2193
  [self detachScrollObserver];
1770
2194
  [self restoreSharedHeadersToContainer];
1771
2195
  if (_blockerGesture != nil) {
@@ -1841,8 +2265,33 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1841
2265
 
1842
2266
  #pragma mark - Smooth shared header ownership
1843
2267
 
2268
+ - (void)restorePinnedStickyHeader
2269
+ {
2270
+ if (!_stickyHeaderIsPinned) return;
2271
+ _stickyHeaderIsPinned = NO;
2272
+ _stickyHeaderView.transform = CGAffineTransformIdentity;
2273
+ _nativeTabBarView.transform = CGAffineTransformIdentity;
2274
+ _nativeSubHeaderView.transform = CGAffineTransformIdentity;
2275
+ [_stickyHeaderView removeFromSuperview];
2276
+ [_nativeTabBarView removeFromSuperview];
2277
+ [_nativeSubHeaderView removeFromSuperview];
2278
+ if (_stickyHeaderView != nil) [_sharedHeaderHostView addSubview:_stickyHeaderView];
2279
+ [_sharedHeaderHostView addSubview:_nativeTabBarView];
2280
+ [_sharedHeaderHostView addSubview:_nativeSubHeaderView];
2281
+ _pinnedStickyHostView.hidden = YES;
2282
+ _pinnedStickyHostView.layer.zPosition = 0;
2283
+ [_containerView addSubview:_pinnedStickyHostView];
2284
+ if ([_pagerScrollView isKindOfClass:RNCCollapsiblePagerDirectScrollView.class]) {
2285
+ ((RNCCollapsiblePagerDirectScrollView *)_pagerScrollView).excludedHeaderView = _sharedHeaderHostView;
2286
+ }
2287
+ [_nativeTabBarView updateAncestorGesturePrecedence];
2288
+ [_nativeSubHeaderView updateAncestorGesturePrecedence];
2289
+ }
2290
+
1844
2291
  - (void)restoreSharedHeadersToContainer
1845
2292
  {
2293
+ [self restorePinnedStickyHeader];
2294
+ [self updateTopScrollEdgeForScrollView:_observedScrollView];
1846
2295
  _sharedHeaderScrollView = nil;
1847
2296
  _sharedHeadersLiftedForPagerTransition = NO;
1848
2297
  _sharedHeaderHostView.layer.zPosition = 0;
@@ -1850,11 +2299,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1850
2299
  [_containerView addSubview:_sharedHeaderHostView];
1851
2300
  }
1852
2301
  [self layoutSharedHeaders];
2302
+ if (self.hasFixedTopOverlay) {
2303
+ [_containerView bringSubviewToFront:_topOverlayHostView];
2304
+ }
1853
2305
  }
1854
2306
 
1855
2307
  - (void)attachSharedHeadersToScrollView:(UIScrollView *)scrollView
1856
2308
  {
1857
2309
  if (!_nativeSmoothHeaderScrollEnabled || _transitioning || _isPagerDragging ||
2310
+ _externalOuterTransitioning ||
1858
2311
  scrollView == nil || _isBeingRecycled) return;
1859
2312
  if ((UIView *)scrollView == _sharedHeaderHostView ||
1860
2313
  [scrollView isDescendantOfView:_sharedHeaderHostView]) {
@@ -1866,13 +2319,16 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1866
2319
  }
1867
2320
  if (_sharedHeaderScrollView == scrollView &&
1868
2321
  _sharedHeaderHostView.superview == scrollView) {
2322
+ _sharedHeaderHostView.layer.zPosition =
2323
+ _topScrollEdgeHard && (self.hasFixedTopOverlay || _stickyHeaderView != nil) ? 0 : 1000;
1869
2324
  [self layoutSharedHeaders];
1870
2325
  return;
1871
2326
  }
1872
2327
 
1873
2328
  _sharedHeaderScrollView = scrollView;
1874
2329
  _sharedHeadersLiftedForPagerTransition = NO;
1875
- _sharedHeaderHostView.layer.zPosition = 1000;
2330
+ _sharedHeaderHostView.layer.zPosition =
2331
+ _topScrollEdgeHard && (self.hasFixedTopOverlay || _stickyHeaderView != nil) ? 0 : 1000;
1876
2332
  if (_sharedHeaderHostView.superview != scrollView) {
1877
2333
  [scrollView addSubview:_sharedHeaderHostView];
1878
2334
  }
@@ -1919,56 +2375,114 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1919
2375
 
1920
2376
  _sharedHeaderHostView.frame = CGRectMake(
1921
2377
  0,
1922
- attachedToList ? -totalHeight : 0,
2378
+ attachedToList ? -totalHeight : self.effectiveTopOverlayHeight,
1923
2379
  width,
1924
2380
  totalHeight
1925
2381
  );
1926
2382
  CGFloat stickyY = _headerHeight;
1927
2383
  _headerView.frame = CGRectMake(0, 0, width, _headerHeight);
1928
- _stickyHeaderView.frame = CGRectMake(0, stickyY, width, _stickyHeaderHeight);
2384
+ CGPoint pinnedOrigin = _stickyHeaderIsPinned
2385
+ ? [_sharedHeaderScrollView convertPoint:CGPointMake(0, self.effectiveTopOverlayHeight) fromView:_containerView]
2386
+ : CGPointMake(0, self.effectiveTopOverlayHeight);
2387
+ _pinnedStickyHostView.frame = CGRectMake(
2388
+ pinnedOrigin.x, pinnedOrigin.y, width, _stickyHeaderHeight);
2389
+ CGFloat stickyChildY = _stickyHeaderIsPinned ? 0 : stickyY;
2390
+ _stickyHeaderView.frame = CGRectMake(0, stickyChildY, width, _stickyHeaderHeight);
1929
2391
  CGFloat nativeTabBarVisibleHeight = _nativeTabBarView.hidden
1930
2392
  ? 0
1931
2393
  : MIN(_stickyHeaderHeight, _nativeTabBarHeight);
1932
2394
  _nativeTabBarView.frame = CGRectMake(
1933
2395
  0,
1934
- stickyY,
2396
+ stickyChildY,
1935
2397
  width,
1936
2398
  nativeTabBarVisibleHeight
1937
2399
  );
1938
2400
  _nativeSubHeaderView.frame = CGRectMake(
1939
2401
  0,
1940
- stickyY + nativeTabBarVisibleHeight,
2402
+ stickyChildY + nativeTabBarVisibleHeight,
1941
2403
  width,
1942
2404
  MIN(
1943
2405
  MAX(0, _stickyHeaderHeight - nativeTabBarVisibleHeight),
1944
2406
  _nativeSubHeaderHeight
1945
2407
  )
1946
2408
  );
2409
+ _topOverlayHostView.frame = CGRectMake(0, 0, width, self.effectiveTopOverlayHeight);
2410
+ _topOverlayView.frame = _topOverlayHostView.bounds;
1947
2411
  [self applyHeaderOffset];
1948
2412
  }
1949
2413
 
1950
- - (void)offsetDiag:(NSString *)reason page:(NSInteger)index scroll:(UIScrollView *)scroll
2414
+ - (void)updatePinnedStickyHeader
1951
2415
  {
1952
- #if DEBUG
1953
- NSString *key = index >= 0 && index < _pageControllers.count ? [self pageKeyForIndex:index] : @"none";
1954
- if (![@[@"portfolio", @"perps", @"defi", @"nft", @"history"] containsObject:key]) key = @"other";
1955
- NSNumber *saved = index >= 0 && index < _pageControllers.count ? _pageOffsets[[self pageKeyForIndex:index]] : nil;
1956
- UIView *component = scroll == nil ? nil : RNCScrollComponentView(scroll);
1957
- RNCCollapsiblePagerLog([NSString stringWithFormat:
1958
- @"[HOME-OFFSET-DIAG] pager phase=%@ seq=%lu wallMs=%.0f mono=%.6f host=%p generation=%lu transitionId=%lu page=%ld key=%@ current=%ld destination=%ld transition=%d dragging=%d scroll=%p tag=%ld component=%p componentTag=%ld observed=%p ownerIndex=%ld ownerGeneration=%lu synthetic=%d cacheExists=%d cache=%.4f header=%.4f headerHeight=%ld physicalY=%.4f insetTop=%.4f insetBottom=%.4f logicalY=%.4f contentHeight=%.4f boundsHeight=%.4f",
1959
- reason, (unsigned long)++_offsetDiagSequence, NSDate.date.timeIntervalSince1970 * 1000, CACurrentMediaTime(),
1960
- (__bridge void *)self, (unsigned long)_generation, (unsigned long)_transitionId, (long)index, key,
1961
- (long)_currentIndex, (long)_destinationIndex, _transitioning, _isPagerDragging,
1962
- (__bridge void *)scroll, (long)scroll.tag, (__bridge void *)component, (long)component.tag,
1963
- (__bridge void *)_observedScrollView, (long)_observedPageIndex, (unsigned long)_observedPageGeneration, _syntheticPageRestore, saved != nil, saved.doubleValue, _headerOffset, (long)_headerHeight,
1964
- scroll.contentOffset.y, scroll.contentInset.top, scroll.contentInset.bottom,
1965
- scroll.contentOffset.y + scroll.contentInset.top, scroll.contentSize.height, scroll.bounds.size.height]);
1966
- #endif
2416
+ BOOL attachedToList = _nativeSmoothHeaderScrollEnabled &&
2417
+ _sharedHeaderScrollView != nil && !_sharedHeadersLiftedForPagerTransition;
2418
+ if (!attachedToList && !_stickyHeaderIsPinned) return;
2419
+ UIScrollView *scrollView = _sharedHeaderScrollView;
2420
+ NSNumber *appliedTop = [_appliedTopInsets objectForKey:scrollView];
2421
+ CGFloat refreshInset = scrollView.refreshControl != nil && appliedTop != nil
2422
+ ? MAX(0, scrollView.contentInset.top - appliedTop.doubleValue) : 0;
2423
+ CGFloat pinnedTranslation = MAX(0, _currentLogicalOffset - _headerHeight - refreshInset);
2424
+ CGFloat stickyTop = [_sharedHeaderHostView convertPoint:
2425
+ CGPointMake(0, _headerHeight + pinnedTranslation) toView:self].y;
2426
+ BOOL shouldPin = _topScrollEdgeHard &&
2427
+ _stickyHeaderView != nil && attachedToList && !_transitioning &&
2428
+ !_externalOuterTransitioning &&
2429
+ !_isPagerDragging && _headerOffset >= _headerHeight - 0.5 &&
2430
+ stickyTop <= self.effectiveTopOverlayHeight + 0.5;
2431
+ if (_stickyHeaderIsPinned && shouldPin && _pinnedStickyHostView.superview != scrollView) {
2432
+ [self updateTopScrollEdgeForScrollView:nil];
2433
+ [scrollView addSubview:_pinnedStickyHostView];
2434
+ [self updateTopScrollEdgeForScrollView:_observedScrollView];
2435
+ }
2436
+ if (_stickyHeaderIsPinned == shouldPin) {
2437
+ if (shouldPin) {
2438
+ CGPoint origin = [scrollView convertPoint:CGPointMake(0, self.effectiveTopOverlayHeight)
2439
+ fromView:_containerView];
2440
+ _pinnedStickyHostView.frame = CGRectMake(origin.x, origin.y,
2441
+ CGRectGetWidth(self.bounds), _stickyHeaderHeight);
2442
+ }
2443
+ return;
2444
+ }
2445
+
2446
+ _stickyHeaderIsPinned = shouldPin;
2447
+ UIView *host = shouldPin ? _pinnedStickyHostView : _sharedHeaderHostView;
2448
+ _stickyHeaderView.transform = CGAffineTransformIdentity;
2449
+ _nativeTabBarView.transform = CGAffineTransformIdentity;
2450
+ _nativeSubHeaderView.transform = CGAffineTransformIdentity;
2451
+ [_stickyHeaderView removeFromSuperview];
2452
+ [_nativeTabBarView removeFromSuperview];
2453
+ [_nativeSubHeaderView removeFromSuperview];
2454
+ if (_stickyHeaderView != nil) [host addSubview:_stickyHeaderView];
2455
+ [host addSubview:_nativeTabBarView];
2456
+ [host addSubview:_nativeSubHeaderView];
2457
+ CGFloat childY = shouldPin ? 0 : _headerHeight;
2458
+ CGFloat width = CGRectGetWidth(self.bounds);
2459
+ CGFloat tabHeight = _nativeTabBarView.hidden ? 0 : MIN(_stickyHeaderHeight, _nativeTabBarHeight);
2460
+ _stickyHeaderView.frame = CGRectMake(0, childY, width, _stickyHeaderHeight);
2461
+ _nativeTabBarView.frame = CGRectMake(0, childY, width, tabHeight);
2462
+ _nativeSubHeaderView.frame = CGRectMake(0, childY + tabHeight, width,
2463
+ MIN(MAX(0, _stickyHeaderHeight - tabHeight), _nativeSubHeaderHeight));
2464
+ _pinnedStickyHostView.hidden = !shouldPin;
2465
+ if ([_pagerScrollView isKindOfClass:RNCCollapsiblePagerDirectScrollView.class]) {
2466
+ ((RNCCollapsiblePagerDirectScrollView *)_pagerScrollView).excludedHeaderView =
2467
+ shouldPin ? _pinnedStickyHostView : _sharedHeaderHostView;
2468
+ }
2469
+ if (shouldPin) {
2470
+ [scrollView addSubview:_pinnedStickyHostView];
2471
+ _pinnedStickyHostView.layer.zPosition = 1000;
2472
+ CGPoint origin = [scrollView convertPoint:CGPointMake(0, self.effectiveTopOverlayHeight)
2473
+ fromView:_containerView];
2474
+ _pinnedStickyHostView.frame = CGRectMake(origin.x, origin.y, width, _stickyHeaderHeight);
2475
+ } else {
2476
+ _pinnedStickyHostView.layer.zPosition = 0;
2477
+ [_containerView addSubview:_pinnedStickyHostView];
2478
+ }
2479
+ [_nativeTabBarView updateAncestorGesturePrecedence];
2480
+ [_nativeSubHeaderView updateAncestorGesturePrecedence];
2481
+ [self updateTopScrollEdgeForScrollView:_observedScrollView];
1967
2482
  }
1968
2483
 
1969
2484
  - (void)preparePageForHorizontalTransitionAtIndex:(NSInteger)pageIndex
1970
2485
  {
1971
- [self offsetDiag:@"prepare-check" page:pageIndex scroll:nil];
1972
2486
  if (!_nativeSmoothHeaderScrollEnabled ||
1973
2487
  pageIndex < 0 || pageIndex >= _pageControllers.count) return;
1974
2488
  UIScrollView *scrollView = [self verticalScrollViewForPageAtIndex:pageIndex];
@@ -1987,11 +2501,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1987
2501
  _transitionHeaderOffset = _headerOffset;
1988
2502
  _transitionPageOffsets = [_pageOffsets copy];
1989
2503
  _transitionCompensatedPages = [NSMutableIndexSet new];
1990
- #if DEBUG
1991
- _transitionDiagBucket = -1;
1992
- [self offsetDiag:[NSString stringWithFormat:@"screen-begin-source=%ld-savedQ=%.4f", (long)_transitionSourceIndex,
1993
- [_transitionPageOffsets[self.currentPageKey] doubleValue]] page:_currentIndex scroll:nil];
1994
- #endif
1995
2504
  }
1996
2505
 
1997
2506
  - (void)updatePageStateTransitionProgress:(CGFloat)progress
@@ -2008,16 +2517,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2008
2517
  CGFloat firstQ = [_transitionPageOffsets[[self pageKeyForIndex:first]] doubleValue];
2009
2518
  CGFloat secondQ = [_transitionPageOffsets[[self pageKeyForIndex:second]] doubleValue];
2010
2519
  _headerOffset = _transitionHeaderOffset;
2011
- #if DEBUG
2012
- NSInteger bucket = (NSInteger)floor(fraction * 4);
2013
- if (_transitionDiagBucket != bucket) {
2014
- _transitionDiagBucket = bucket;
2015
- [self offsetDiag:[NSString stringWithFormat:
2016
- @"screen-progress-kind=%@-source=%ld-target=%ld-sourceQ=%.4f-targetQ=%.4f-position=%.4f-fraction=%.4f",
2017
- _isPagerDragging ? @"interactive" : @"programmatic-endpoint", (long)first, (long)second,
2018
- firstQ, secondQ, progress, fraction] page:_currentIndex scroll:nil];
2019
- }
2020
- #endif
2021
2520
  _syntheticPageRestore = YES;
2022
2521
  NSInteger visibleFirst = MAX(0, MIN((NSInteger)floor(progress), (NSInteger)_pageControllers.count - 1));
2023
2522
  NSInteger visibleLast = MIN(visibleFirst + 1, (NSInteger)_pageControllers.count - 1);
@@ -2059,10 +2558,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2059
2558
  _transitionPageOffsets = nil;
2060
2559
  _transitionCompensatedPages = nil;
2061
2560
  [self layoutSharedHeaders];
2062
- #if DEBUG
2063
- [self offsetDiag:[NSString stringWithFormat:@"screen-finish-source=%ld-cancelled=%d", (long)_transitionSourceIndex,
2064
- _currentIndex == _transitionSourceIndex] page:_currentIndex scroll:nil];
2065
- #endif
2066
2561
  }
2067
2562
 
2068
2563
  #pragma mark - React mounting
@@ -2119,9 +2614,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2119
2614
  if (!_nativeSmoothHeaderScrollEnabled && _transitionPageOffsets == nil && !_syntheticPageRestore &&
2120
2615
  ![_pendingPageRestoreKeys containsObject:pageKey] &&
2121
2616
  (ownsObservedPage || _pageOffsets[pageKey] == nil)) {
2122
- [self offsetDiag:@"mount-release-save-before" page:index scroll:scrollView];
2123
2617
  _pageOffsets[pageKey] = @(scrollView.contentOffset.y + scrollView.contentInset.top);
2124
- [self offsetDiag:@"mount-release-save-after" page:index scroll:scrollView];
2125
2618
  }
2126
2619
  break;
2127
2620
  }
@@ -2137,6 +2630,27 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2137
2630
  - (void)mountingTransactionDidMount:(const MountingTransaction &)transaction
2138
2631
  withSurfaceTelemetry:(const SurfaceTelemetry &)surfaceTelemetry
2139
2632
  {
2633
+ if (_needsSlotRebuild && !_deferTopEdgeSlotRebuild &&
2634
+ !_transitioning && !_isPagerDragging) {
2635
+ [self rebuildSlots];
2636
+ }
2637
+ [self registerExternalOuterPager];
2638
+ if ([self resolveExternalTopOverlay]) {
2639
+ [self reapplyInsetsToObservedScrollView];
2640
+ [self setNeedsLayout];
2641
+ }
2642
+ if (_pendingTopOverlayAppearanceCommit) {
2643
+ _pendingTopOverlayAppearanceCommit = NO;
2644
+ BOOL localOverlayReady = _topOverlayView.superview == _topOverlayHostView &&
2645
+ _topOverlayView.window == self.window;
2646
+ BOOL externalOverlayReady = _externalTopOverlayTag > 0 &&
2647
+ _externalTopOverlayView.window == self.window;
2648
+ if (!_isBeingRecycled && _topScrollEdgeHard && self.window != nil &&
2649
+ (localOverlayReady || externalOverlayReady)) {
2650
+ [self updateTopScrollEdgeForScrollView:_observedScrollView];
2651
+ }
2652
+ }
2653
+ [self updateScrollEdgeContentMasks];
2140
2654
  [_mountingRemovedScrollViews removeAllObjects];
2141
2655
  if (!_nativeSmoothHeaderScrollEnabled || _isBeingRecycled || self.window == nil ||
2142
2656
  _currentIndex < 0 || _currentIndex >= _pageControllers.count) return;
@@ -2214,7 +2728,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2214
2728
  if (_isBeingRecycled) return;
2215
2729
  NSInteger safeIndex = MIN(MAX(index, 0), _logicalChildren.count);
2216
2730
  [_logicalChildren insertObject:childComponentView atIndex:safeIndex];
2217
- [self rebuildSlots];
2731
+ if (_topScrollEdgeHard || _topOverlayView != nil) {
2732
+ _needsSlotRebuild = YES;
2733
+ } else {
2734
+ [self rebuildSlots];
2735
+ }
2218
2736
  }
2219
2737
 
2220
2738
  - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
@@ -2225,16 +2743,32 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2225
2743
  if (existingIndex == NSNotFound) return;
2226
2744
  [childComponentView removeFromSuperview];
2227
2745
  [_logicalChildren removeObjectAtIndex:existingIndex];
2228
- [self rebuildSlots];
2746
+ if (_topScrollEdgeHard || _topOverlayView != nil) {
2747
+ _needsSlotRebuild = YES;
2748
+ } else {
2749
+ [self rebuildSlots];
2750
+ }
2229
2751
  }
2230
2752
 
2231
2753
  - (void)rebuildSlots
2232
2754
  {
2755
+ if (_deferTopEdgeSlotRebuild) {
2756
+ _needsSlotRebuild = YES;
2757
+ return;
2758
+ }
2233
2759
  if (_transitioning || _isPagerDragging) {
2234
2760
  _needsSlotRebuild = YES;
2235
2761
  return;
2236
2762
  }
2763
+ if (_topScrollEdgeHard &&
2764
+ _logicalChildren.count != _pageKeys.count + 2 &&
2765
+ _logicalChildren.count != _pageKeys.count + 3) {
2766
+ _needsSlotRebuild = YES;
2767
+ return;
2768
+ }
2237
2769
  _needsSlotRebuild = NO;
2770
+ _deferTopEdgeSlotRebuild = NO;
2771
+ [self restoreScrollEdgeContentMasks];
2238
2772
  [self detachScrollObserver];
2239
2773
  [self restoreSharedHeadersToContainer];
2240
2774
  // OneKey patch: Fabric reuses these views without resetting transforms
@@ -2246,11 +2780,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2246
2780
  _nativeSubHeaderView.transform = CGAffineTransformIdentity;
2247
2781
  [_headerView removeFromSuperview];
2248
2782
  [_stickyHeaderView removeFromSuperview];
2783
+ [self updateTopScrollEdgeForScrollView:nil];
2784
+ [_topOverlayView removeFromSuperview];
2249
2785
  for (UIViewController *controller in _pageControllers) {
2250
2786
  [controller.view removeFromSuperview];
2251
2787
  }
2252
2788
  _headerView = nil;
2253
2789
  _stickyHeaderView = nil;
2790
+ _topOverlayView = nil;
2791
+ [self updateTopScrollEdgeForScrollView:nil];
2254
2792
  [_pageControllers removeAllObjects];
2255
2793
 
2256
2794
  if (_logicalChildren.count > 0) {
@@ -2261,7 +2799,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2261
2799
  _stickyHeaderView = _logicalChildren[1];
2262
2800
  [_sharedHeaderHostView addSubview:_stickyHeaderView];
2263
2801
  }
2264
- for (NSInteger index = 2; index < _logicalChildren.count; index++) {
2802
+ BOOL hasOverlaySlot = _topScrollEdgeHard &&
2803
+ _logicalChildren.count == _pageKeys.count + 3;
2804
+ NSInteger pageSlotStart = hasOverlaySlot ? 3 : 2;
2805
+ if (hasOverlaySlot) {
2806
+ _topOverlayView = _logicalChildren[2];
2807
+ [_topOverlayHostView addSubview:_topOverlayView];
2808
+ }
2809
+ [self resolveExternalTopOverlay];
2810
+ for (NSInteger index = pageSlotStart; index < _logicalChildren.count; index++) {
2265
2811
  UIView *page = _logicalChildren[index];
2266
2812
  UIViewController *controller = [UIViewController new];
2267
2813
  [controller.view addSubview:page];
@@ -2300,6 +2846,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2300
2846
  if (!_nativeTabBarView.hidden) [_sharedHeaderHostView bringSubviewToFront:_nativeTabBarView];
2301
2847
  if (!_nativeSubHeaderView.hidden) [_sharedHeaderHostView bringSubviewToFront:_nativeSubHeaderView];
2302
2848
  [_containerView bringSubviewToFront:_sharedHeaderHostView];
2849
+ if (_stickyHeaderIsPinned) {
2850
+ [_pinnedStickyHostView.superview bringSubviewToFront:_pinnedStickyHostView];
2851
+ }
2852
+ if (self.hasFixedTopOverlay) [_containerView bringSubviewToFront:_topOverlayHostView];
2303
2853
  [self setNeedsLayout];
2304
2854
  dispatch_async(dispatch_get_main_queue(), ^{
2305
2855
  if (!self->_isBeingRecycled) {
@@ -2312,7 +2862,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2312
2862
  - (void)layoutSubviews
2313
2863
  {
2314
2864
  [super layoutSubviews];
2865
+ [self syncScrollEdgeAppearance];
2315
2866
  _containerView.frame = self.bounds;
2867
+ [self registerExternalOuterPager];
2868
+ BOOL externalHeightChanged = [self resolveExternalTopOverlay];
2316
2869
  if (_directNativePagerEnabled) {
2317
2870
  _pagerScrollView.frame = _containerView.bounds;
2318
2871
  } else {
@@ -2375,6 +2928,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2375
2928
  }
2376
2929
  }
2377
2930
  [self layoutSharedHeaders];
2931
+ if (externalHeightChanged) [self reapplyInsetsToObservedScrollView];
2932
+ [self updateTopScrollEdgeForScrollView:_observedScrollView];
2933
+ [self updateScrollEdgeContentMasks];
2378
2934
  [self updateHeaderSettledStateForceBusy:NO];
2379
2935
  dispatch_async(dispatch_get_main_queue(), ^{
2380
2936
  if (!self->_isBeingRecycled) {
@@ -2386,9 +2942,16 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2386
2942
 
2387
2943
  - (void)prepareForRecycle
2388
2944
  {
2945
+ [_externalOuterPager removeNestedOverlayObserver:self];
2946
+ _externalOuterPager = nil;
2947
+ [self cancelTopInsetReboundCheck];
2948
+ [self restoreScrollEdgeContentMasks];
2949
+ [self updateTopScrollEdgeForScrollView:nil];
2389
2950
  [self cancelHeaderSettledCheck];
2390
2951
  [self publishHeaderSettledAtTop:NO identityChanged:NO];
2391
2952
  _isBeingRecycled = YES;
2953
+ _pendingTopOverlayAppearanceCommit = NO;
2954
+ [self syncScrollEdgeAppearance];
2392
2955
  _generation++;
2393
2956
  [_mountingRemovedScrollViews removeAllObjects];
2394
2957
  _needsScrollObserverReattachAfterMount = NO;
@@ -2408,9 +2971,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2408
2971
  if (appliedTop != nil && scrollView.refreshControl != nil) {
2409
2972
  original.top += MAX(0, scrollView.contentInset.top - appliedTop.doubleValue);
2410
2973
  }
2411
- [self offsetDiag:@"release-original-inset-before" page:_currentIndex scroll:scrollView];
2412
2974
  scrollView.contentInset = original;
2413
- [self offsetDiag:@"release-original-inset-after" page:_currentIndex scroll:scrollView];
2414
2975
  }
2415
2976
  NSValue *indicatorValue = [_originalIndicatorInsets objectForKey:scrollView];
2416
2977
  if (indicatorValue != nil) {
@@ -2439,6 +3000,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2439
3000
  _nativeSubHeaderView.transform = CGAffineTransformIdentity;
2440
3001
  [_headerView removeFromSuperview];
2441
3002
  [_stickyHeaderView removeFromSuperview];
3003
+ [_topOverlayView removeFromSuperview];
2442
3004
  for (UIViewController *controller in _pageControllers) {
2443
3005
  [controller.view removeFromSuperview];
2444
3006
  }
@@ -2446,6 +3008,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2446
3008
  [_pageControllers removeAllObjects];
2447
3009
  _headerView = nil;
2448
3010
  _stickyHeaderView = nil;
3011
+ _topOverlayView = nil;
3012
+ _topOverlayHostView.hidden = YES;
3013
+ [self updateTopScrollEdgeForScrollView:nil];
2449
3014
  _pageViewController.dataSource = nil;
2450
3015
  _pageViewController.delegate = nil;
2451
3016
  _pagerScrollView.delegate = nil;
@@ -2478,6 +3043,13 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2478
3043
  _pendingInitialPage = 0;
2479
3044
  _headerHeight = 0;
2480
3045
  _stickyHeaderHeight = 0;
3046
+ _topOverlayHeight = 0;
3047
+ _externalTopOverlayTag = 0;
3048
+ _externalTopOverlayView = nil;
3049
+ _externalTopOverlayHeight = 0;
3050
+ _externalOuterPageActive = NO;
3051
+ _externalOuterTransitioning = NO;
3052
+ _topScrollEdgeHard = NO;
2481
3053
  _nativeTabBarHeight = 44;
2482
3054
  _nativeSubHeaderHeight = 74;
2483
3055
  _headerOffset = 0;
@@ -2501,6 +3073,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2501
3073
  _pendingGoToIndex = -1;
2502
3074
  _pendingGoToAnimated = YES;
2503
3075
  _needsSlotRebuild = NO;
3076
+ _deferTopEdgeSlotRebuild = NO;
2504
3077
  _hasAppliedInitialPage = NO;
2505
3078
  // OneKey patch: Fabric retains old props when recycling this view.
2506
3079
  _needsPropsReapply = YES;
@@ -2517,6 +3090,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2517
3090
  {
2518
3091
  const auto &oldViewProps = *std::static_pointer_cast<const RNCCollapsiblePagerViewProps>(_props);
2519
3092
  const auto &newViewProps = *std::static_pointer_cast<const RNCCollapsiblePagerViewProps>(props);
3093
+ _applyingTopOverlayInsetChange =
3094
+ oldViewProps.topScrollEdgeHard != newViewProps.topScrollEdgeHard ||
3095
+ oldViewProps.topOverlayHeight != newViewProps.topOverlayHeight;
2520
3096
 
2521
3097
  if (_needsPropsReapply || oldViewProps.initialPage != newViewProps.initialPage) {
2522
3098
  _pendingInitialPage = MAX(0, newViewProps.initialPage);
@@ -2566,6 +3142,43 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2566
3142
  _stickyHeaderHeight = MAX(0, newViewProps.stickyHeaderHeight);
2567
3143
  insetsChanged = YES;
2568
3144
  }
3145
+ if (_needsPropsReapply || oldViewProps.topScrollEdgeHard != newViewProps.topScrollEdgeHard) {
3146
+ _topScrollEdgeHard = newViewProps.topScrollEdgeHard;
3147
+ if (oldViewProps.topScrollEdgeHard || newViewProps.topScrollEdgeHard) {
3148
+ _deferTopEdgeSlotRebuild = YES;
3149
+ [self rebuildSlots];
3150
+ NSUInteger generation = _generation;
3151
+ dispatch_async(dispatch_get_main_queue(), ^{
3152
+ if (self->_generation != generation || self->_isBeingRecycled ||
3153
+ !self->_deferTopEdgeSlotRebuild) return;
3154
+ self->_deferTopEdgeSlotRebuild = NO;
3155
+ [self rebuildSlots];
3156
+ });
3157
+ }
3158
+ insetsChanged = YES;
3159
+ }
3160
+ if (_needsPropsReapply ||
3161
+ oldViewProps.topOverlayColorScheme != newViewProps.topOverlayColorScheme) {
3162
+ NSString *nextScheme = RCTNSStringFromString(newViewProps.topOverlayColorScheme);
3163
+ if (self.hasFixedTopOverlay && _topScrollEdgeHard &&
3164
+ _topOverlayColorScheme != nil &&
3165
+ ![_topOverlayColorScheme isEqualToString:nextScheme]) {
3166
+ _pendingTopOverlayAppearanceCommit = YES;
3167
+ [self updateTopScrollEdgeForScrollView:nil];
3168
+ }
3169
+ _topOverlayColorScheme = nextScheme;
3170
+ [self syncScrollEdgeAppearance];
3171
+ }
3172
+ if (_needsPropsReapply || oldViewProps.topOverlayHeight != newViewProps.topOverlayHeight) {
3173
+ _topOverlayHeight = MAX(0, newViewProps.topOverlayHeight);
3174
+ insetsChanged = YES;
3175
+ }
3176
+ if (_needsPropsReapply ||
3177
+ oldViewProps.externalTopOverlayTag != newViewProps.externalTopOverlayTag) {
3178
+ _externalTopOverlayTag = newViewProps.externalTopOverlayTag;
3179
+ [self registerExternalOuterPager];
3180
+ insetsChanged = [self resolveExternalTopOverlay] || insetsChanged;
3181
+ }
2569
3182
  if (_needsPropsReapply || oldViewProps.pageKeys != newViewProps.pageKeys) {
2570
3183
  NSString *json = RCTNSStringFromString(newViewProps.pageKeys);
2571
3184
  NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding];
@@ -2633,17 +3246,23 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2633
3246
 
2634
3247
  _needsPropsReapply = NO;
2635
3248
  [super updateProps:props oldProps:oldProps];
3249
+ [self updateTopScrollEdgeForScrollView:_observedScrollView];
2636
3250
  if (insetsChanged) {
2637
3251
  [self reapplyInsetsToObservedScrollView];
2638
3252
  [self setNeedsLayout];
2639
3253
  }
3254
+ _applyingTopOverlayInsetChange = NO;
2640
3255
  if (!_nativeTabBarView.hidden) {
2641
- [_sharedHeaderHostView bringSubviewToFront:_nativeTabBarView];
3256
+ [_nativeTabBarView.superview bringSubviewToFront:_nativeTabBarView];
2642
3257
  }
2643
3258
  if (!_nativeSubHeaderView.hidden) {
2644
- [_sharedHeaderHostView bringSubviewToFront:_nativeSubHeaderView];
3259
+ [_nativeSubHeaderView.superview bringSubviewToFront:_nativeSubHeaderView];
2645
3260
  }
2646
3261
  [_sharedHeaderHostView.superview bringSubviewToFront:_sharedHeaderHostView];
3262
+ if (_stickyHeaderIsPinned) {
3263
+ [_pinnedStickyHostView.superview bringSubviewToFront:_pinnedStickyHostView];
3264
+ }
3265
+ if (self.hasFixedTopOverlay) [_containerView bringSubviewToFront:_topOverlayHostView];
2647
3266
  }
2648
3267
 
2649
3268
  #pragma mark - Collapsible scroll coordination
@@ -2877,21 +3496,17 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2877
3496
  return;
2878
3497
  }
2879
3498
 
2880
- [self offsetDiag:@"observer-candidate" page:_currentIndex scroll:candidate];
2881
3499
  BOOL replacingCurrentScrollView = _observingContentOffset &&
2882
3500
  (!_nativeSmoothHeaderScrollEnabled || [_observedPageKey isEqualToString:self.currentPageKey]);
2883
3501
  [self detachScrollObserver];
2884
3502
  if (replacingCurrentScrollView) {
2885
3503
  // Legacy replacement discards body state; smooth replacement waits for layout.
2886
- [self offsetDiag:@"observer-replacement-cache-before" page:_currentIndex scroll:candidate];
2887
3504
  if (!_nativeSmoothHeaderScrollEnabled) _pageOffsets[self.currentPageKey] = @(_headerOffset);
2888
- [self offsetDiag:@"observer-replacement-cache-after" page:_currentIndex scroll:candidate];
2889
3505
  }
2890
3506
  _observedScrollView = candidate;
2891
3507
  _observedPageKey = [self.currentPageKey copy];
2892
3508
  _observedPageIndex = _currentIndex;
2893
3509
  _observedPageGeneration = _generation;
2894
- [self offsetDiag:@"observer-attached-before-restore" page:_currentIndex scroll:candidate];
2895
3510
  [self applyInsetsToScrollView:candidate pageIndex:_currentIndex restore:YES];
2896
3511
  [candidate addObserver:self
2897
3512
  forKeyPath:@"contentOffset"
@@ -2909,6 +3524,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2909
3524
  _observingContentOffset = YES;
2910
3525
  _needsScrollObserverReattachAfterMount = NO;
2911
3526
  _currentLogicalOffset = candidate.contentOffset.y + candidate.contentInset.top;
3527
+ [self updateTopScrollEdgeForScrollView:candidate];
2912
3528
  [self attachSharedHeadersToScrollView:candidate];
2913
3529
  [self updateSharedHeaderPressCancellationGesture];
2914
3530
  [self updateHeaderSettledStateForceBusy:NO];
@@ -2916,6 +3532,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2916
3532
 
2917
3533
  - (void)detachScrollObserver
2918
3534
  {
3535
+ [self cancelTopInsetReboundCheck];
3536
+ [self restorePinnedStickyHeader];
3537
+ [self updateTopScrollEdgeForScrollView:nil];
2919
3538
  [self cancelHeaderSettledCheck];
2920
3539
  _headerCommandOwner = nil;
2921
3540
  _headerSettledGeometry = nil;
@@ -2925,7 +3544,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2925
3544
  UIScrollView *scrollView = _observedScrollView;
2926
3545
  if (scrollView != nil) {
2927
3546
  [scrollView.panGestureRecognizer removeTarget:self action:@selector(observedListPanChanged:)];
2928
- [self offsetDiag:@"detach-save-before" page:_currentIndex scroll:scrollView];
2929
3547
  NSString *ownerKey = _nativeSmoothHeaderScrollEnabled ? _observedPageKey : self.currentPageKey;
2930
3548
  BOOL ownerKeyExists = ownerKey != nil && ([_pageKeys containsObject:ownerKey] ||
2931
3549
  (_observedPageIndex >= 0 && _observedPageIndex < _pageControllers.count &&
@@ -2933,7 +3551,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2933
3551
  if (ownerKey != nil && !_nativeSmoothHeaderScrollEnabled) {
2934
3552
  _pageOffsets[ownerKey] = @(scrollView.contentOffset.y + scrollView.contentInset.top);
2935
3553
  }
2936
- [self offsetDiag:@"detach-save-after" page:_currentIndex scroll:scrollView];
2937
3554
  if (_observingContentOffset) {
2938
3555
  [scrollView removeObserver:self
2939
3556
  forKeyPath:@"contentOffset"
@@ -2975,6 +3592,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2975
3592
  removeGestureRecognizer:_sharedHeaderOuterPagerGesture];
2976
3593
  }
2977
3594
  _sharedHeaderOuterPagerGesture.blockedPagerGestures = nil;
3595
+ _sharedHeaderOuterPagerGesture.trackedTouch = nil;
3596
+ _sharedHeaderOuterPagerGesture.protectsNestedBody = NO;
3597
+ _sharedHeaderOuterPagerGesture.cancelsTouchesInView = YES;
2978
3598
  _sharedHeaderPressCancellationGestureHost = nil;
2979
3599
  _reactTouchHandler = nil;
2980
3600
  }
@@ -3024,7 +3644,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3024
3644
  }
3025
3645
  }
3026
3646
  if (_sharedHeaderPressCancellationGesture.view == gestureHost &&
3027
- _sharedHeaderOuterPagerGesture.view == _sharedHeaderHostView &&
3647
+ _sharedHeaderOuterPagerGesture.view == _containerView &&
3028
3648
  _reactTouchHandler == reactTouchHandler) {
3029
3649
  _sharedHeaderOuterPagerGesture.blockedPagerGestures = outerPagerGestures;
3030
3650
  return;
@@ -3066,7 +3686,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3066
3686
  _reactTouchHandler = reactTouchHandler;
3067
3687
  _sharedHeaderPressCancellationGestureHost = gestureHost;
3068
3688
  [gestureHost addGestureRecognizer:_sharedHeaderPressCancellationGesture];
3069
- [_sharedHeaderHostView addGestureRecognizer:_sharedHeaderOuterPagerGesture];
3689
+ [_containerView addGestureRecognizer:_sharedHeaderOuterPagerGesture];
3070
3690
  RNCCollapsiblePagerLog([NSString stringWithFormat:
3071
3691
  @"press-cancel-bridge attached=1 outer-pagers=%lu",
3072
3692
  (unsigned long)outerPagerGestures.count]);
@@ -3138,6 +3758,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3138
3758
 
3139
3759
  - (void)observedListPanChanged:(UIPanGestureRecognizer *)recognizer
3140
3760
  {
3761
+ if (recognizer == _pendingTopInsetReboundScrollView.panGestureRecognizer &&
3762
+ (recognizer.state == UIGestureRecognizerStateBegan ||
3763
+ recognizer.state == UIGestureRecognizerStateChanged)) {
3764
+ [self cancelTopInsetReboundCheck];
3765
+ }
3141
3766
  if (recognizer == _observedScrollView.panGestureRecognizer) {
3142
3767
  [self updateHeaderSettledStateForceBusy:
3143
3768
  recognizer.state == UIGestureRecognizerStateBegan || recognizer.state == UIGestureRecognizerStateChanged];
@@ -3205,7 +3830,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3205
3830
  [_managedRefreshControls removeObjectForKey:scrollView];
3206
3831
  if (current != nil) [_managedRefreshControls setObject:current forKey:scrollView];
3207
3832
  }
3208
- [RNCRefreshControlOffsetState setOffset:_headerHeight + _stickyHeaderHeight
3833
+ [RNCRefreshControlOffsetState setOffset:self.effectiveTopOverlayHeight + _headerHeight + _stickyHeaderHeight
3209
3834
  forControl:current owner:self];
3210
3835
  }
3211
3836
 
@@ -3219,6 +3844,53 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3219
3844
  [_managedRefreshControls removeObjectForKey:scrollView];
3220
3845
  }
3221
3846
 
3847
+ - (void)cancelTopInsetReboundCheck
3848
+ {
3849
+ [_topInsetReboundTimer invalidate];
3850
+ _topInsetReboundTimer = nil;
3851
+ _pendingTopInsetReboundScrollView = nil;
3852
+ _pendingTopInsetReboundPageKey = nil;
3853
+ }
3854
+
3855
+ - (void)scheduleTopInsetReboundCheck
3856
+ {
3857
+ if (_pendingTopInsetReboundScrollView == nil ||
3858
+ _pendingTopInsetReboundScrollView != _observedScrollView) return;
3859
+ [_topInsetReboundTimer invalidate];
3860
+ __weak __typeof__(self) weakSelf = self;
3861
+ _topInsetReboundTimer = [NSTimer scheduledTimerWithTimeInterval:0.15 repeats:NO block:^(NSTimer *timer) {
3862
+ __strong __typeof__(weakSelf) self = weakSelf;
3863
+ if (self == nil || self->_topInsetReboundTimer != timer) return;
3864
+ self->_topInsetReboundTimer = nil;
3865
+ UIScrollView *scrollView = self->_pendingTopInsetReboundScrollView;
3866
+ if (scrollView == nil || scrollView != self->_observedScrollView ||
3867
+ self->_pendingTopInsetReboundGeneration != self->_generation ||
3868
+ ![self->_pendingTopInsetReboundPageKey isEqualToString:self.currentPageKey] ||
3869
+ self->_isBeingRecycled || self.window == nil ||
3870
+ self->_transitioning || self->_isPagerDragging ||
3871
+ self->_headerCommandOwner == scrollView || self->_pagerTopPageKey != nil) {
3872
+ [self cancelTopInsetReboundCheck];
3873
+ return;
3874
+ }
3875
+ UIGestureRecognizerState pan = scrollView.panGestureRecognizer.state;
3876
+ if (scrollView.refreshControl.isRefreshing || scrollView.isDragging ||
3877
+ scrollView.isDecelerating || scrollView.isTracking ||
3878
+ pan == UIGestureRecognizerStateBegan || pan == UIGestureRecognizerStateChanged ||
3879
+ self->_syntheticPageRestore || self->_transitionPageOffsets != nil) {
3880
+ [self scheduleTopInsetReboundCheck];
3881
+ return;
3882
+ }
3883
+ CGFloat minimumOffset = -scrollView.adjustedContentInset.top;
3884
+ [self cancelTopInsetReboundCheck];
3885
+ if (scrollView.contentOffset.y < minimumOffset - 0.5) {
3886
+ // UIKit can finish a refresh rebound at the inset that preceded an overlay resize.
3887
+ self->_correctingTopInsetRebound = YES;
3888
+ [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, minimumOffset) animated:NO];
3889
+ self->_correctingTopInsetRebound = NO;
3890
+ }
3891
+ }];
3892
+ }
3893
+
3222
3894
  // OneKey patch: release observers, headers and saved state while the scroll view
3223
3895
  // still represents the old list, before Fabric applies its next owner's props.
3224
3896
  - (void)restoreScrollViewState:(UIScrollView *)scrollView resetSavedOffset:(BOOL)resetSavedOffset
@@ -3227,9 +3899,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3227
3899
  [self releaseRefreshControlForScrollView:scrollView];
3228
3900
  if (scrollView == _observedScrollView) {
3229
3901
  [self detachScrollObserver];
3230
- [self offsetDiag:resetSavedOffset ? @"release-reset-before" : @"release-retain-before" page:_currentIndex scroll:scrollView];
3231
3902
  if (resetSavedOffset && !_nativeSmoothHeaderScrollEnabled) _pageOffsets[self.currentPageKey] = @(_headerOffset);
3232
- [self offsetDiag:@"release-reset-after" page:_currentIndex scroll:scrollView];
3233
3903
  }
3234
3904
  if (scrollView == _sharedHeaderScrollView) {
3235
3905
  [self restoreSharedHeadersToContainer];
@@ -3249,10 +3919,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3249
3919
  [_originalDirectionalLockEnabled objectForKey:scrollView].boolValue;
3250
3920
  scrollView.contentInsetAdjustmentBehavior = (UIScrollViewContentInsetAdjustmentBehavior)
3251
3921
  [_originalInsetAdjustmentBehavior objectForKey:scrollView].integerValue;
3252
- [self offsetDiag:@"release-original-offset-before" page:_currentIndex scroll:scrollView];
3253
3922
  [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, logicalOffset - original.top)
3254
3923
  animated:NO];
3255
- [self offsetDiag:@"release-original-offset-after" page:_currentIndex scroll:scrollView];
3256
3924
  [_originalInsets removeObjectForKey:scrollView];
3257
3925
  [_appliedTopInsets removeObjectForKey:scrollView];
3258
3926
  [_originalIndicatorInsets removeObjectForKey:scrollView];
@@ -3283,7 +3951,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3283
3951
  {
3284
3952
  // Smooth pages retain their complete offset while replacement identity and
3285
3953
  // layout settle. Legacy pages retain the historical header-only policy.
3286
- [self offsetDiag:restore ? @"restore-enter" : @"insets-enter" page:pageIndex scroll:scrollView];
3287
3954
  NSString *pageKey = [self pageKeyForIndex:pageIndex];
3288
3955
  RNCCollapsiblePagerReleasedScrollState *released = _releasedPageScrollStates[pageKey];
3289
3956
  BOOL deferredReleasedIdentity = NO;
@@ -3329,13 +3996,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3329
3996
  deferredReleasedIdentity = now - released.pendingFallbackDetectedAt <
3330
3997
  RNCCollapsiblePagerFallbackDebounceInterval;
3331
3998
  }
3332
- #if DEBUG
3333
- [self offsetDiag:[NSString stringWithFormat:@"released-identity-match=%d-deferred=%d-oldTag=%ld-oldScroll=%p", sameList, deferredReleasedIdentity, (long)released.componentTag, (__bridge void *)released.scrollView] page:pageIndex scroll:scrollView];
3334
- #endif
3335
3999
  if (!deferredReleasedIdentity) {
3336
- [self offsetDiag:@"released-identity-cache-before" page:pageIndex scroll:scrollView];
3337
4000
  if (!sameList && !_nativeSmoothHeaderScrollEnabled) _pageOffsets[pageKey] = @(_headerOffset);
3338
- [self offsetDiag:@"released-identity-cache-after" page:pageIndex scroll:scrollView];
3339
4001
  [_releasedPageScrollStates removeObjectForKey:pageKey];
3340
4002
  }
3341
4003
  }
@@ -3360,22 +4022,24 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3360
4022
  // suppress its later vertical movement while the pager is still pending.
3361
4023
  scrollView.directionalLockEnabled = _nativeSmoothHeaderScrollEnabled
3362
4024
  ? NO : [_originalDirectionalLockEnabled objectForKey:scrollView].boolValue;
3363
- [self updateNativeScrollerViewport:scrollView stickyHeight:_stickyHeaderHeight];
4025
+ [self updateNativeScrollerViewport:scrollView stickyHeight:_stickyHeaderHeight + self.effectiveTopOverlayHeight];
3364
4026
  [self updateRefreshControlForScrollView:scrollView];
3365
4027
  CGFloat previousTopInset = scrollView.contentInset.top;
3366
4028
  CGFloat previousLogicalOffset = scrollView.contentOffset.y + previousTopInset;
4029
+ // Preserve UIKit's rebound after refresh while the content is beyond its top inset.
4030
+ BOOL wasReturningFromPull = scrollView.contentOffset.y < -scrollView.adjustedContentInset.top - 0.5;
3367
4031
  NSNumber *appliedTop = [_appliedTopInsets objectForKey:scrollView];
3368
4032
  // UIRefreshControl owns its temporary top inset, including while ending.
3369
4033
  CGFloat refreshInset = appliedTop != nil && scrollView.refreshControl != nil
3370
4034
  ? MAX(0, previousTopInset - appliedTop.doubleValue) : 0;
3371
4035
  UIEdgeInsets next = original;
3372
- next.top += _headerHeight + _stickyHeaderHeight;
4036
+ next.top += self.effectiveTopOverlayHeight + _headerHeight + _stickyHeaderHeight;
3373
4037
  CGFloat pagerTopInset = next.top;
3374
4038
  next.top += refreshInset;
3375
4039
  [_appliedTopInsets setObject:@(pagerTopInset) forKey:scrollView];
3376
4040
  // Short pages still need enough range to collapse the header; long pages
3377
4041
  // must end at the last row rather than leave a header-sized blank footer.
3378
- next.bottom += MAX(0, scrollView.bounds.size.height - _stickyHeaderHeight
4042
+ next.bottom += MAX(0, scrollView.bounds.size.height - _stickyHeaderHeight - self.effectiveTopOverlayHeight
3379
4043
  - original.top - original.bottom - scrollView.contentSize.height);
3380
4044
  if (UIEdgeInsetsEqualToEdgeInsets(scrollView.contentInset, next) && !restore) {
3381
4045
  // UICollectionView may report contentSize during a bounce without changing
@@ -3385,9 +4049,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3385
4049
  }
3386
4050
  BOOL previousSyntheticRestore = _syntheticPageRestore;
3387
4051
  if (_nativeSmoothHeaderScrollEnabled || [_alignedBodyKeys containsObject:pageKey]) _syntheticPageRestore = YES;
3388
- [self offsetDiag:@"inset-assignment-before" page:pageIndex scroll:scrollView];
3389
4052
  scrollView.contentInset = next;
3390
- [self offsetDiag:@"inset-assignment-after" page:pageIndex scroll:scrollView];
3391
4053
  UIEdgeInsets indicatorInsets = scrollView.verticalScrollIndicatorInsets;
3392
4054
  indicatorInsets.top = pagerTopInset;
3393
4055
  scrollView.verticalScrollIndicatorInsets = indicatorInsets;
@@ -3419,9 +4081,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3419
4081
  } else {
3420
4082
  restoredLogicalOffset = deferredReleasedIdentity || saved == nil ? _headerOffset : saved.doubleValue;
3421
4083
  }
3422
- #if DEBUG
3423
- [self offsetDiag:[NSString stringWithFormat:@"restore-candidate-saved=%.4f-headerAlignedLogical=%.4f", saved.doubleValue, restoredLogicalOffset] page:pageIndex scroll:scrollView];
3424
- #endif
3425
4084
  intendedLogicalOffset = restoredLogicalOffset;
3426
4085
  // Cancellation restores the visible source, while retaining its unreachable reading intent.
3427
4086
  if (_nativeSmoothHeaderScrollEnabled && _headerOffset >= _headerHeight &&
@@ -3434,9 +4093,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3434
4093
  }
3435
4094
  // An inactive list may shrink after its offset observer was detached.
3436
4095
  // Do not overwrite its corrected offset with a now-unreachable cached one.
3437
- #if DEBUG
3438
- [self offsetDiag:[NSString stringWithFormat:@"restore-clamp-before-candidate=%.4f-max=%.4f", targetOffset, MAX(-next.top, scrollView.contentSize.height - scrollView.bounds.size.height + next.bottom)] page:pageIndex scroll:scrollView];
3439
- #endif
3440
4096
  targetOffset = RNCLimitRestoredScrollOffset(
3441
4097
  targetOffset,
3442
4098
  scrollView.contentSize.height,
@@ -3445,13 +4101,17 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3445
4101
  next.bottom
3446
4102
  );
3447
4103
  }
3448
- #if DEBUG
3449
- [self offsetDiag:[NSString stringWithFormat:@"restore-final-target=%.4f-restore=%d", targetOffset, restore] page:pageIndex scroll:scrollView];
3450
- #endif
3451
4104
  // A bottom-inset change must not cancel UIKit's refresh or bounce settlement.
3452
- if (restore || previousTopInset != next.top) {
4105
+ if (restore || (previousTopInset != next.top && !wasReturningFromPull)) {
3453
4106
  [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, targetOffset) animated:NO];
3454
4107
  }
4108
+ if (_applyingTopOverlayInsetChange && !restore && wasReturningFromPull &&
4109
+ previousTopInset != next.top && scrollView == _observedScrollView) {
4110
+ _pendingTopInsetReboundScrollView = scrollView;
4111
+ _pendingTopInsetReboundPageKey = [pageKey copy];
4112
+ _pendingTopInsetReboundGeneration = _generation;
4113
+ [self scheduleTopInsetReboundCheck];
4114
+ }
3455
4115
  if (_nativeSmoothHeaderScrollEnabled && restore && targetOffset + next.top + 0.5 < intendedLogicalOffset) {
3456
4116
  [_pendingPageRestoreKeys addObject:pageKey];
3457
4117
  }
@@ -3461,7 +4121,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3461
4121
  if (!_nativeSmoothHeaderScrollEnabled) [self updateHeaderForScrollView:scrollView];
3462
4122
  else [self applyHeaderOffset];
3463
4123
  }
3464
- [self offsetDiag:@"restore-post" page:pageIndex scroll:scrollView];
3465
4124
  }
3466
4125
 
3467
4126
  - (void)updateNativeScrollerViewport:(UIScrollView *)scrollView stickyHeight:(CGFloat)height
@@ -3478,6 +4137,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3478
4137
  _observedPageKey == nil || ![_observedPageKey isEqualToString:self.currentPageKey] ||
3479
4138
  _observedPageIndex != _currentIndex || _observedPageGeneration != _generation ||
3480
4139
  _transitioning || _isPagerDragging || _transitionPageOffsets != nil || _syntheticPageRestore) return;
4140
+ [self cancelTopInsetReboundCheck];
3481
4141
  NSInteger phase = [notification.userInfo[@"phase"] integerValue];
3482
4142
  [self updateHeaderSettledStateForceBusy:NO];
3483
4143
  if (phase == 1) _headerCommandOwner = scrollView;
@@ -3510,7 +4170,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3510
4170
  _headerOffset = 0;
3511
4171
  [self applyHeaderOffset];
3512
4172
  }
3513
- [self offsetDiag:@"list-command-body-takeover" page:_currentIndex scroll:scrollView];
3514
4173
  }
3515
4174
 
3516
4175
  - (void)nativeScrollerWillReceiveScrollCommand:(RNCNativeScrollerComponentView *)scroller
@@ -3522,12 +4181,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3522
4181
  _observedPageIndex != _currentIndex || _currentIndex < 0 || _currentIndex >= _pageControllers.count ||
3523
4182
  _observedPageGeneration != _generation || _transitioning || _isPagerDragging ||
3524
4183
  _transitionPageOffsets != nil || _syntheticPageRestore) return;
4184
+ [self cancelTopInsetReboundCheck];
3525
4185
  if (!_dispatchingPagerTop) {
3526
4186
  [self cancelPagerTop];
3527
4187
  [_alignedBodyKeys removeObject:ownerKey];
3528
4188
  }
3529
4189
  if (!_nativeSmoothHeaderScrollEnabled) return;
3530
- [self offsetDiag:@"command-pending-cancel-before" page:_observedPageIndex scroll:scrollView];
3531
4190
  [_pendingPageRestoreKeys removeObject:ownerKey];
3532
4191
  _currentLogicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
3533
4192
  [_commandBodyKeys removeObject:ownerKey];
@@ -3536,7 +4195,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3536
4195
  [_displayBodyOverrides removeObjectForKey:ownerKey];
3537
4196
  _pageOffsets[ownerKey] = @(MAX(_currentLogicalOffset - _headerHeight, 0));
3538
4197
  [self updateHeaderForScrollView:scrollView];
3539
- [self offsetDiag:@"command-pending-cancel-after" page:_observedPageIndex scroll:scrollView];
3540
4198
  }
3541
4199
 
3542
4200
 
@@ -3598,6 +4256,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3598
4256
  return;
3599
4257
  }
3600
4258
  if (context == RNCCollapsiblePagerContentOffsetContext && object == _observedScrollView) {
4259
+ if (_pendingTopInsetReboundScrollView == object &&
4260
+ ([keyPath isEqualToString:@"contentOffset"] || [keyPath isEqualToString:@"contentInset"])) {
4261
+ if (((UIScrollView *)object).panGestureRecognizer.state == UIGestureRecognizerStateChanged) {
4262
+ [self cancelTopInsetReboundCheck];
4263
+ } else {
4264
+ [self scheduleTopInsetReboundCheck];
4265
+ }
4266
+ }
3601
4267
  if (![change[NSKeyValueChangeOldKey] isEqual:change[NSKeyValueChangeNewKey]]) {
3602
4268
  [self updateHeaderSettledStateForceBusy:YES];
3603
4269
  }
@@ -3662,18 +4328,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3662
4328
  }
3663
4329
  UIScrollView *scrollView = (UIScrollView *)object;
3664
4330
  _currentLogicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
3665
- #if DEBUG
3666
- BOOL offsetDiagChanged = _pageOffsets[self.currentPageKey] == nil || _pageOffsets[self.currentPageKey].doubleValue != (_nativeSmoothHeaderScrollEnabled ? MAX(_currentLogicalOffset - _headerHeight, 0) : _currentLogicalOffset);
3667
- if (offsetDiagChanged) [self offsetDiag:@"kvo-cache-before" page:_currentIndex scroll:scrollView];
3668
- #endif
4331
+ if (_correctingTopInsetRebound) return;
3669
4332
  if ([_alignedBodyKeys containsObject:_observedPageKey] ||
3670
4333
  (_nativeSmoothHeaderScrollEnabled && scrollView.panGestureRecognizer.state != UIGestureRecognizerStateChanged &&
3671
4334
  !scrollView.isDecelerating && ![_commandBodyKeys containsObject:_observedPageKey])) return;
3672
4335
  _pageOffsets[_nativeSmoothHeaderScrollEnabled ? _observedPageKey : self.currentPageKey] =
3673
4336
  @(_nativeSmoothHeaderScrollEnabled ? MAX(_currentLogicalOffset - _headerHeight, 0) : _currentLogicalOffset);
3674
- #if DEBUG
3675
- if (offsetDiagChanged) [self offsetDiag:@"kvo-cache-after" page:_currentIndex scroll:scrollView];
3676
- #endif
3677
4337
  [self updateHeaderForScrollView:scrollView];
3678
4338
  return;
3679
4339
  }
@@ -3714,17 +4374,22 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3714
4374
  ? MAX(0, scrollView.contentInset.top - appliedTop.doubleValue) : 0;
3715
4375
  CGFloat pinnedTranslation = MAX(0, _currentLogicalOffset - _headerHeight - refreshInset);
3716
4376
  CGAffineTransform stickyTransform = CGAffineTransformMakeTranslation(0, pinnedTranslation);
3717
- _stickyHeaderView.transform = stickyTransform;
3718
- _nativeTabBarView.transform = stickyTransform;
3719
- _nativeSubHeaderView.transform = stickyTransform;
4377
+ [self updatePinnedStickyHeader];
4378
+ _stickyHeaderView.transform = _stickyHeaderIsPinned ? CGAffineTransformIdentity : stickyTransform;
4379
+ _nativeTabBarView.transform = _stickyHeaderIsPinned ? CGAffineTransformIdentity : stickyTransform;
4380
+ _nativeSubHeaderView.transform = _stickyHeaderIsPinned ? CGAffineTransformIdentity : stickyTransform;
4381
+ [self updateScrollEdgeContentMasks];
3720
4382
  return;
3721
4383
  }
4384
+ [self updatePinnedStickyHeader];
3722
4385
  CGAffineTransform collapseTransform = CGAffineTransformMakeTranslation(0, -_headerOffset);
3723
4386
  _sharedHeaderHostView.transform = collapseTransform;
3724
4387
  _headerView.transform = CGAffineTransformIdentity;
3725
4388
  _stickyHeaderView.transform = CGAffineTransformIdentity;
3726
4389
  _nativeTabBarView.transform = CGAffineTransformIdentity;
3727
4390
  _nativeSubHeaderView.transform = CGAffineTransformIdentity;
4391
+ [self updateTopScrollEdgeForScrollView:_observedScrollView];
4392
+ [self updateScrollEdgeContentMasks];
3728
4393
  }
3729
4394
 
3730
4395
  #pragma mark - Pager navigation
@@ -3736,7 +4401,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3736
4401
 
3737
4402
  - (void)goTo:(NSInteger)index animated:(BOOL)animated
3738
4403
  {
3739
- [self offsetDiag:@"go-to" page:index scroll:_observedScrollView];
4404
+ [self cancelTopInsetReboundCheck];
3740
4405
  if (_isBeingRecycled || index < 0 || index >= _pageControllers.count) return;
3741
4406
  _hasReceivedPageCommand = YES;
3742
4407
  _hasAppliedInitialPage = YES;
@@ -3867,6 +4532,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3867
4532
  if (!self->_transitioning && !self->_needsSlotRebuild) {
3868
4533
  [self attachScrollObserverForCurrentPage];
3869
4534
  }
4535
+ [self updateScrollEdgeContentMasks];
3870
4536
  });
3871
4537
  }
3872
4538
 
@@ -3918,7 +4584,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3918
4584
  - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
3919
4585
  {
3920
4586
  [self updateHeaderSettledStateForceBusy:YES];
3921
- [self offsetDiag:@"pager-drag-begin" page:_currentIndex scroll:_observedScrollView];
3922
4587
  if (_nativeSmoothHeaderScrollEnabled && scrollView == _pagerScrollView) {
3923
4588
  [_contentPressCancellationGesture cancelPressForPan:scrollView.panGestureRecognizer];
3924
4589
  }
@@ -4075,6 +4740,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4075
4740
  CGFloat offset = progress - position;
4076
4741
  position = MAX(0, MIN(position, (NSInteger)_pageControllers.count - 1));
4077
4742
  [self updatePageStateTransitionProgress:progress];
4743
+ [self updateScrollEdgeContentMasks];
4078
4744
  [_nativeTabBarView setProgress:progress];
4079
4745
  const auto emitter = [self eventEmitter];
4080
4746
  if (emitter) {
@@ -4093,6 +4759,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4093
4759
  tabProgress = _currentIndex + (_destinationIndex - _currentIndex) * transitionProgress;
4094
4760
  }
4095
4761
  [self updatePageStateTransitionProgress:tabProgress];
4762
+ [self updateScrollEdgeContentMasks];
4096
4763
  [_nativeTabBarView setProgress:tabProgress];
4097
4764
  const auto emitter = [self eventEmitter];
4098
4765
  if (emitter) {
@@ -4110,7 +4777,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4110
4777
 
4111
4778
  - (void)emitPageSelected:(NSInteger)position
4112
4779
  {
4113
- [self offsetDiag:@"selection-complete" page:position scroll:_observedScrollView];
4114
4780
  const auto emitter = [self eventEmitter];
4115
4781
  if (emitter) emitter->onPageSelected({.position = (double)position});
4116
4782
  }
@@ -4255,12 +4921,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4255
4921
  _currentLogicalOffset = logical;
4256
4922
  _headerOffset = _pagerTopLogicalTarget;
4257
4923
  [self applyHeaderOffset];
4258
- [self offsetDiag:[NSString stringWithFormat:@"pager-top-complete-token=%lu-target=%.4f", (unsigned long)_pagerTopToken, _pagerTopLogicalTarget] page:_currentIndex scroll:scrollView];
4259
4924
  [self cancelPagerTop];
4260
4925
  }
4261
4926
 
4262
4927
  - (void)scrollToTop:(NSInteger)target animated:(BOOL)animated
4263
4928
  {
4929
+ [self cancelTopInsetReboundCheck];
4264
4930
  UIScrollView *scrollView = _observedScrollView;
4265
4931
  NSString *key = _observedPageKey;
4266
4932
  if ((target != 0 && target != 1) || _isBeingRecycled || _transitioning || _isPagerDragging ||
@@ -4292,7 +4958,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4292
4958
  [self applyHeaderOffset];
4293
4959
  CGFloat physicalTarget = RNCLimitRestoredScrollOffset(logicalTarget - scrollView.contentInset.top,
4294
4960
  scrollView.contentSize.height, scrollView.bounds.size.height, scrollView.contentInset.top, scrollView.contentInset.bottom);
4295
- [self offsetDiag:[NSString stringWithFormat:@"pager-top-begin-token=%lu-target=%ld-animated=%d", (unsigned long)_pagerTopToken, (long)target, animated] page:_currentIndex scroll:scrollView];
4296
4961
  _dispatchingPagerTop = YES;
4297
4962
  [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, physicalTarget) animated:animated];
4298
4963
  _dispatchingPagerTop = NO;
@@ -4357,6 +5022,22 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4357
5022
  : velocity;
4358
5023
  BOOL hasMotion = MAX(fabs(intent.x), fabs(intent.y)) >= 1;
4359
5024
  BOOL horizontal = hasMotion && fabs(intent.x) > fabs(intent.y);
5025
+ if (_sharedHeaderOuterPagerGesture.protectsNestedBody) {
5026
+ NSInteger pageCount = _pageControllers.count;
5027
+ BOOL validPage = _currentIndex >= 0 && _currentIndex < pageCount;
5028
+ NSInteger firstPageIndex = self.isLtrLayout ? 0 : pageCount - 1;
5029
+ NSInteger lastPageIndex = self.isLtrLayout ? pageCount - 1 : 0;
5030
+ BOOL outward = validPage &&
5031
+ ((_currentIndex == firstPageIndex && intent.x > 0) ||
5032
+ (_currentIndex == lastPageIndex && intent.x < 0));
5033
+ UIScrollView *outer = RNCNestedOuterReactPagerScrollView(self);
5034
+ BOOL protectInnerPage = horizontal && validPage && pageCount > 1 &&
5035
+ _nestedScrollEnabled && _scrollEnabled && _pagerScrollView.scrollEnabled &&
5036
+ _pagerScrollView.panGestureRecognizer.enabled && outer != nil && outer.scrollEnabled &&
5037
+ [_sharedHeaderOuterPagerGesture.blockedPagerGestures
5038
+ containsObject:outer.panGestureRecognizer] && !outward;
5039
+ return protectInnerPage;
5040
+ }
4360
5041
  RNCCollapsiblePagerLog([NSString stringWithFormat:
4361
5042
  @"header-axis role=outer-pager-block dx=%.2f dy=%.2f velocity=(%.2f,%.2f) result=%d outer-pagers=%lu",
4362
5043
  intent.x,
@@ -4396,24 +5077,21 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4396
5077
  return vertical;
4397
5078
  }
4398
5079
  if (gestureRecognizer != _blockerGesture) return YES;
4399
- if (!_nestedScrollEnabled) return NO;
4400
- if (![gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) return NO;
4401
-
4402
- UIPanGestureRecognizer *pan = (UIPanGestureRecognizer *)gestureRecognizer;
4403
- CGPoint velocity = [pan velocityInView:self];
5080
+ UIPanGestureRecognizer *pan = [gestureRecognizer isKindOfClass:UIPanGestureRecognizer.class]
5081
+ ? (UIPanGestureRecognizer *)gestureRecognizer : nil;
5082
+ CGPoint velocity = pan != nil ? [pan velocityInView:self] : CGPointZero;
4404
5083
  NSInteger pageCount = _pageControllers.count;
4405
- if (pageCount == 0 || _currentIndex < 0 || _currentIndex >= pageCount) return NO;
4406
-
4407
5084
  CGFloat absVelocityX = fabs(velocity.x);
4408
5085
  CGFloat absVelocityY = fabs(velocity.y);
4409
- if (absVelocityX <= absVelocityY || absVelocityX < 1.0f) return NO;
4410
-
4411
- BOOL isLtr = self.isLtrLayout;
4412
- NSInteger firstPageIndex = isLtr ? 0 : pageCount - 1;
4413
- NSInteger lastPageIndex = isLtr ? pageCount - 1 : 0;
4414
- if (_currentIndex == firstPageIndex && velocity.x > 0) return YES;
4415
- if (_currentIndex == lastPageIndex && velocity.x < 0) return YES;
4416
- return NO;
5086
+ NSInteger firstPageIndex = self.isLtrLayout ? 0 : pageCount - 1;
5087
+ NSInteger lastPageIndex = self.isLtrLayout ? pageCount - 1 : 0;
5088
+ BOOL validPage = pageCount > 0 && _currentIndex >= 0 && _currentIndex < pageCount;
5089
+ BOOL horizontal = absVelocityX > absVelocityY && absVelocityX >= 1.0f;
5090
+ BOOL outward = validPage &&
5091
+ ((_currentIndex == firstPageIndex && velocity.x > 0) ||
5092
+ (_currentIndex == lastPageIndex && velocity.x < 0));
5093
+ BOOL shouldBegin = _nestedScrollEnabled && pan != nil && horizontal && outward;
5094
+ return shouldBegin;
4417
5095
  }
4418
5096
 
4419
5097
  - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
@@ -4423,13 +5101,29 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4423
5101
  _sharedHeaderPressCancellationGestureHost == nil) return NO;
4424
5102
  UIView *touchView = touch.view;
4425
5103
  BOOL beginsInSharedHeader = touchView == _sharedHeaderHostView ||
4426
- [touchView isDescendantOfView:_sharedHeaderHostView];
4427
- if (!beginsInSharedHeader) return NO;
5104
+ [touchView isDescendantOfView:_sharedHeaderHostView] ||
5105
+ touchView == _pinnedStickyHostView ||
5106
+ [touchView isDescendantOfView:_pinnedStickyHostView];
5107
+ if (_sharedHeaderOuterPagerGesture.trackedTouch != nil &&
5108
+ _sharedHeaderOuterPagerGesture.trackedTouch != touch) return NO;
5109
+ UIScrollView *outer = RNCNestedOuterReactPagerScrollView(self);
5110
+ BOOL beginsInNestedBody = !beginsInSharedHeader && _nestedScrollEnabled &&
5111
+ _scrollEnabled && _pagerScrollView.scrollEnabled &&
5112
+ _pagerScrollView.panGestureRecognizer.enabled && _pageControllers.count > 1 &&
5113
+ outer != nil && outer.scrollEnabled &&
5114
+ [_sharedHeaderOuterPagerGesture.blockedPagerGestures containsObject:outer.panGestureRecognizer] &&
5115
+ (touchView == _pagerScrollView || [touchView isDescendantOfView:_pagerScrollView]);
5116
+ if (!beginsInSharedHeader && !beginsInNestedBody) return NO;
5117
+
5118
+ _sharedHeaderOuterPagerGesture.trackedTouch = touch;
5119
+ _sharedHeaderOuterPagerGesture.protectsNestedBody = beginsInNestedBody;
5120
+ _sharedHeaderOuterPagerGesture.cancelsTouchesInView = !beginsInNestedBody;
4428
5121
 
4429
5122
  RNCCollapsiblePagerLog([NSString stringWithFormat:
4430
- @"outer-pager-touch target=%@ outer-pagers=%lu",
5123
+ @"outer-pager-touch target=%@ outer-pagers=%lu nested-body=%d",
4431
5124
  touchView == nil ? @"none" : NSStringFromClass(touchView.class),
4432
- (unsigned long)_sharedHeaderOuterPagerGesture.blockedPagerGestures.count]);
5125
+ (unsigned long)_sharedHeaderOuterPagerGesture.blockedPagerGestures.count,
5126
+ beginsInNestedBody]);
4433
5127
  return YES;
4434
5128
  }
4435
5129
  if (gestureRecognizer == _contentPressCancellationGesture) {
@@ -4438,14 +5132,17 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4438
5132
  return _nativeSmoothHeaderScrollEnabled && !_isBeingRecycled &&
4439
5133
  _contentPressCancellationGesture.view != nil &&
4440
5134
  [touchView isDescendantOfView:_pagerScrollView] &&
4441
- ![touchView isDescendantOfView:_sharedHeaderHostView];
5135
+ ![touchView isDescendantOfView:_sharedHeaderHostView] &&
5136
+ ![touchView isDescendantOfView:_pinnedStickyHostView];
4442
5137
  }
4443
5138
  if (gestureRecognizer == _sharedHeaderPressCancellationGesture) {
4444
5139
  if (!_nativeSmoothHeaderScrollEnabled || _isBeingRecycled ||
4445
5140
  _sharedHeaderPressCancellationGestureHost == nil) return NO;
4446
5141
  UIView *touchView = touch.view;
4447
5142
  BOOL beginsInSharedHeader = touchView == _sharedHeaderHostView ||
4448
- [touchView isDescendantOfView:_sharedHeaderHostView];
5143
+ [touchView isDescendantOfView:_sharedHeaderHostView] ||
5144
+ touchView == _pinnedStickyHostView ||
5145
+ [touchView isDescendantOfView:_pinnedStickyHostView];
4449
5146
  if (!beginsInSharedHeader) return NO;
4450
5147
 
4451
5148
  RNCCollapsiblePagerLog([NSString stringWithFormat:
@@ -4456,7 +5153,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4456
5153
  if (gestureRecognizer == _verticalPagerGesture) {
4457
5154
  UIView *touchView = touch.view;
4458
5155
  return touchView != _sharedHeaderHostView &&
4459
- ![touchView isDescendantOfView:_sharedHeaderHostView];
5156
+ ![touchView isDescendantOfView:_sharedHeaderHostView] &&
5157
+ touchView != _pinnedStickyHostView &&
5158
+ ![touchView isDescendantOfView:_pinnedStickyHostView];
4460
5159
  }
4461
5160
  // A new touch can cancel UIKit's animation before its first frame. Queue a
4462
5161
  // non-animated settle for the next safe transition boundary instead of