@onekeyfe/react-native-pager-view 3.0.177 → 3.0.179-alpha.20261015

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,41 @@ 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
+ _deferTopEdgeSlotRebuild = YES;
3148
+ [self rebuildSlots];
3149
+ NSUInteger generation = _generation;
3150
+ dispatch_async(dispatch_get_main_queue(), ^{
3151
+ if (self->_generation != generation || self->_isBeingRecycled ||
3152
+ !self->_deferTopEdgeSlotRebuild) return;
3153
+ self->_deferTopEdgeSlotRebuild = NO;
3154
+ [self rebuildSlots];
3155
+ });
3156
+ insetsChanged = YES;
3157
+ }
3158
+ if (_needsPropsReapply ||
3159
+ oldViewProps.topOverlayColorScheme != newViewProps.topOverlayColorScheme) {
3160
+ NSString *nextScheme = RCTNSStringFromString(newViewProps.topOverlayColorScheme);
3161
+ if (self.hasFixedTopOverlay && _topScrollEdgeHard &&
3162
+ _topOverlayColorScheme != nil &&
3163
+ ![_topOverlayColorScheme isEqualToString:nextScheme]) {
3164
+ _pendingTopOverlayAppearanceCommit = YES;
3165
+ [self updateTopScrollEdgeForScrollView:nil];
3166
+ }
3167
+ _topOverlayColorScheme = nextScheme;
3168
+ [self syncScrollEdgeAppearance];
3169
+ }
3170
+ if (_needsPropsReapply || oldViewProps.topOverlayHeight != newViewProps.topOverlayHeight) {
3171
+ _topOverlayHeight = MAX(0, newViewProps.topOverlayHeight);
3172
+ insetsChanged = YES;
3173
+ }
3174
+ if (_needsPropsReapply ||
3175
+ oldViewProps.externalTopOverlayTag != newViewProps.externalTopOverlayTag) {
3176
+ _externalTopOverlayTag = newViewProps.externalTopOverlayTag;
3177
+ [self registerExternalOuterPager];
3178
+ insetsChanged = [self resolveExternalTopOverlay] || insetsChanged;
3179
+ }
2569
3180
  if (_needsPropsReapply || oldViewProps.pageKeys != newViewProps.pageKeys) {
2570
3181
  NSString *json = RCTNSStringFromString(newViewProps.pageKeys);
2571
3182
  NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding];
@@ -2633,17 +3244,23 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2633
3244
 
2634
3245
  _needsPropsReapply = NO;
2635
3246
  [super updateProps:props oldProps:oldProps];
3247
+ [self updateTopScrollEdgeForScrollView:_observedScrollView];
2636
3248
  if (insetsChanged) {
2637
3249
  [self reapplyInsetsToObservedScrollView];
2638
3250
  [self setNeedsLayout];
2639
3251
  }
3252
+ _applyingTopOverlayInsetChange = NO;
2640
3253
  if (!_nativeTabBarView.hidden) {
2641
- [_sharedHeaderHostView bringSubviewToFront:_nativeTabBarView];
3254
+ [_nativeTabBarView.superview bringSubviewToFront:_nativeTabBarView];
2642
3255
  }
2643
3256
  if (!_nativeSubHeaderView.hidden) {
2644
- [_sharedHeaderHostView bringSubviewToFront:_nativeSubHeaderView];
3257
+ [_nativeSubHeaderView.superview bringSubviewToFront:_nativeSubHeaderView];
2645
3258
  }
2646
3259
  [_sharedHeaderHostView.superview bringSubviewToFront:_sharedHeaderHostView];
3260
+ if (_stickyHeaderIsPinned) {
3261
+ [_pinnedStickyHostView.superview bringSubviewToFront:_pinnedStickyHostView];
3262
+ }
3263
+ if (self.hasFixedTopOverlay) [_containerView bringSubviewToFront:_topOverlayHostView];
2647
3264
  }
2648
3265
 
2649
3266
  #pragma mark - Collapsible scroll coordination
@@ -2877,21 +3494,17 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2877
3494
  return;
2878
3495
  }
2879
3496
 
2880
- [self offsetDiag:@"observer-candidate" page:_currentIndex scroll:candidate];
2881
3497
  BOOL replacingCurrentScrollView = _observingContentOffset &&
2882
3498
  (!_nativeSmoothHeaderScrollEnabled || [_observedPageKey isEqualToString:self.currentPageKey]);
2883
3499
  [self detachScrollObserver];
2884
3500
  if (replacingCurrentScrollView) {
2885
3501
  // Legacy replacement discards body state; smooth replacement waits for layout.
2886
- [self offsetDiag:@"observer-replacement-cache-before" page:_currentIndex scroll:candidate];
2887
3502
  if (!_nativeSmoothHeaderScrollEnabled) _pageOffsets[self.currentPageKey] = @(_headerOffset);
2888
- [self offsetDiag:@"observer-replacement-cache-after" page:_currentIndex scroll:candidate];
2889
3503
  }
2890
3504
  _observedScrollView = candidate;
2891
3505
  _observedPageKey = [self.currentPageKey copy];
2892
3506
  _observedPageIndex = _currentIndex;
2893
3507
  _observedPageGeneration = _generation;
2894
- [self offsetDiag:@"observer-attached-before-restore" page:_currentIndex scroll:candidate];
2895
3508
  [self applyInsetsToScrollView:candidate pageIndex:_currentIndex restore:YES];
2896
3509
  [candidate addObserver:self
2897
3510
  forKeyPath:@"contentOffset"
@@ -2909,6 +3522,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2909
3522
  _observingContentOffset = YES;
2910
3523
  _needsScrollObserverReattachAfterMount = NO;
2911
3524
  _currentLogicalOffset = candidate.contentOffset.y + candidate.contentInset.top;
3525
+ [self updateTopScrollEdgeForScrollView:candidate];
2912
3526
  [self attachSharedHeadersToScrollView:candidate];
2913
3527
  [self updateSharedHeaderPressCancellationGesture];
2914
3528
  [self updateHeaderSettledStateForceBusy:NO];
@@ -2916,6 +3530,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2916
3530
 
2917
3531
  - (void)detachScrollObserver
2918
3532
  {
3533
+ [self cancelTopInsetReboundCheck];
3534
+ [self restorePinnedStickyHeader];
3535
+ [self updateTopScrollEdgeForScrollView:nil];
2919
3536
  [self cancelHeaderSettledCheck];
2920
3537
  _headerCommandOwner = nil;
2921
3538
  _headerSettledGeometry = nil;
@@ -2925,7 +3542,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2925
3542
  UIScrollView *scrollView = _observedScrollView;
2926
3543
  if (scrollView != nil) {
2927
3544
  [scrollView.panGestureRecognizer removeTarget:self action:@selector(observedListPanChanged:)];
2928
- [self offsetDiag:@"detach-save-before" page:_currentIndex scroll:scrollView];
2929
3545
  NSString *ownerKey = _nativeSmoothHeaderScrollEnabled ? _observedPageKey : self.currentPageKey;
2930
3546
  BOOL ownerKeyExists = ownerKey != nil && ([_pageKeys containsObject:ownerKey] ||
2931
3547
  (_observedPageIndex >= 0 && _observedPageIndex < _pageControllers.count &&
@@ -2933,7 +3549,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2933
3549
  if (ownerKey != nil && !_nativeSmoothHeaderScrollEnabled) {
2934
3550
  _pageOffsets[ownerKey] = @(scrollView.contentOffset.y + scrollView.contentInset.top);
2935
3551
  }
2936
- [self offsetDiag:@"detach-save-after" page:_currentIndex scroll:scrollView];
2937
3552
  if (_observingContentOffset) {
2938
3553
  [scrollView removeObserver:self
2939
3554
  forKeyPath:@"contentOffset"
@@ -2975,6 +3590,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2975
3590
  removeGestureRecognizer:_sharedHeaderOuterPagerGesture];
2976
3591
  }
2977
3592
  _sharedHeaderOuterPagerGesture.blockedPagerGestures = nil;
3593
+ _sharedHeaderOuterPagerGesture.trackedTouch = nil;
3594
+ _sharedHeaderOuterPagerGesture.protectsNestedBody = NO;
3595
+ _sharedHeaderOuterPagerGesture.cancelsTouchesInView = YES;
2978
3596
  _sharedHeaderPressCancellationGestureHost = nil;
2979
3597
  _reactTouchHandler = nil;
2980
3598
  }
@@ -3024,7 +3642,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3024
3642
  }
3025
3643
  }
3026
3644
  if (_sharedHeaderPressCancellationGesture.view == gestureHost &&
3027
- _sharedHeaderOuterPagerGesture.view == _sharedHeaderHostView &&
3645
+ _sharedHeaderOuterPagerGesture.view == _containerView &&
3028
3646
  _reactTouchHandler == reactTouchHandler) {
3029
3647
  _sharedHeaderOuterPagerGesture.blockedPagerGestures = outerPagerGestures;
3030
3648
  return;
@@ -3066,7 +3684,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3066
3684
  _reactTouchHandler = reactTouchHandler;
3067
3685
  _sharedHeaderPressCancellationGestureHost = gestureHost;
3068
3686
  [gestureHost addGestureRecognizer:_sharedHeaderPressCancellationGesture];
3069
- [_sharedHeaderHostView addGestureRecognizer:_sharedHeaderOuterPagerGesture];
3687
+ [_containerView addGestureRecognizer:_sharedHeaderOuterPagerGesture];
3070
3688
  RNCCollapsiblePagerLog([NSString stringWithFormat:
3071
3689
  @"press-cancel-bridge attached=1 outer-pagers=%lu",
3072
3690
  (unsigned long)outerPagerGestures.count]);
@@ -3138,6 +3756,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3138
3756
 
3139
3757
  - (void)observedListPanChanged:(UIPanGestureRecognizer *)recognizer
3140
3758
  {
3759
+ if (recognizer == _pendingTopInsetReboundScrollView.panGestureRecognizer &&
3760
+ (recognizer.state == UIGestureRecognizerStateBegan ||
3761
+ recognizer.state == UIGestureRecognizerStateChanged)) {
3762
+ [self cancelTopInsetReboundCheck];
3763
+ }
3141
3764
  if (recognizer == _observedScrollView.panGestureRecognizer) {
3142
3765
  [self updateHeaderSettledStateForceBusy:
3143
3766
  recognizer.state == UIGestureRecognizerStateBegan || recognizer.state == UIGestureRecognizerStateChanged];
@@ -3205,7 +3828,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3205
3828
  [_managedRefreshControls removeObjectForKey:scrollView];
3206
3829
  if (current != nil) [_managedRefreshControls setObject:current forKey:scrollView];
3207
3830
  }
3208
- [RNCRefreshControlOffsetState setOffset:_headerHeight + _stickyHeaderHeight
3831
+ [RNCRefreshControlOffsetState setOffset:self.effectiveTopOverlayHeight + _headerHeight + _stickyHeaderHeight
3209
3832
  forControl:current owner:self];
3210
3833
  }
3211
3834
 
@@ -3219,6 +3842,53 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3219
3842
  [_managedRefreshControls removeObjectForKey:scrollView];
3220
3843
  }
3221
3844
 
3845
+ - (void)cancelTopInsetReboundCheck
3846
+ {
3847
+ [_topInsetReboundTimer invalidate];
3848
+ _topInsetReboundTimer = nil;
3849
+ _pendingTopInsetReboundScrollView = nil;
3850
+ _pendingTopInsetReboundPageKey = nil;
3851
+ }
3852
+
3853
+ - (void)scheduleTopInsetReboundCheck
3854
+ {
3855
+ if (_pendingTopInsetReboundScrollView == nil ||
3856
+ _pendingTopInsetReboundScrollView != _observedScrollView) return;
3857
+ [_topInsetReboundTimer invalidate];
3858
+ __weak __typeof__(self) weakSelf = self;
3859
+ _topInsetReboundTimer = [NSTimer scheduledTimerWithTimeInterval:0.15 repeats:NO block:^(NSTimer *timer) {
3860
+ __strong __typeof__(weakSelf) self = weakSelf;
3861
+ if (self == nil || self->_topInsetReboundTimer != timer) return;
3862
+ self->_topInsetReboundTimer = nil;
3863
+ UIScrollView *scrollView = self->_pendingTopInsetReboundScrollView;
3864
+ if (scrollView == nil || scrollView != self->_observedScrollView ||
3865
+ self->_pendingTopInsetReboundGeneration != self->_generation ||
3866
+ ![self->_pendingTopInsetReboundPageKey isEqualToString:self.currentPageKey] ||
3867
+ self->_isBeingRecycled || self.window == nil ||
3868
+ self->_transitioning || self->_isPagerDragging ||
3869
+ self->_headerCommandOwner == scrollView || self->_pagerTopPageKey != nil) {
3870
+ [self cancelTopInsetReboundCheck];
3871
+ return;
3872
+ }
3873
+ UIGestureRecognizerState pan = scrollView.panGestureRecognizer.state;
3874
+ if (scrollView.refreshControl.isRefreshing || scrollView.isDragging ||
3875
+ scrollView.isDecelerating || scrollView.isTracking ||
3876
+ pan == UIGestureRecognizerStateBegan || pan == UIGestureRecognizerStateChanged ||
3877
+ self->_syntheticPageRestore || self->_transitionPageOffsets != nil) {
3878
+ [self scheduleTopInsetReboundCheck];
3879
+ return;
3880
+ }
3881
+ CGFloat minimumOffset = -scrollView.adjustedContentInset.top;
3882
+ [self cancelTopInsetReboundCheck];
3883
+ if (scrollView.contentOffset.y < minimumOffset - 0.5) {
3884
+ // UIKit can finish a refresh rebound at the inset that preceded an overlay resize.
3885
+ self->_correctingTopInsetRebound = YES;
3886
+ [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, minimumOffset) animated:NO];
3887
+ self->_correctingTopInsetRebound = NO;
3888
+ }
3889
+ }];
3890
+ }
3891
+
3222
3892
  // OneKey patch: release observers, headers and saved state while the scroll view
3223
3893
  // still represents the old list, before Fabric applies its next owner's props.
3224
3894
  - (void)restoreScrollViewState:(UIScrollView *)scrollView resetSavedOffset:(BOOL)resetSavedOffset
@@ -3227,9 +3897,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3227
3897
  [self releaseRefreshControlForScrollView:scrollView];
3228
3898
  if (scrollView == _observedScrollView) {
3229
3899
  [self detachScrollObserver];
3230
- [self offsetDiag:resetSavedOffset ? @"release-reset-before" : @"release-retain-before" page:_currentIndex scroll:scrollView];
3231
3900
  if (resetSavedOffset && !_nativeSmoothHeaderScrollEnabled) _pageOffsets[self.currentPageKey] = @(_headerOffset);
3232
- [self offsetDiag:@"release-reset-after" page:_currentIndex scroll:scrollView];
3233
3901
  }
3234
3902
  if (scrollView == _sharedHeaderScrollView) {
3235
3903
  [self restoreSharedHeadersToContainer];
@@ -3249,10 +3917,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3249
3917
  [_originalDirectionalLockEnabled objectForKey:scrollView].boolValue;
3250
3918
  scrollView.contentInsetAdjustmentBehavior = (UIScrollViewContentInsetAdjustmentBehavior)
3251
3919
  [_originalInsetAdjustmentBehavior objectForKey:scrollView].integerValue;
3252
- [self offsetDiag:@"release-original-offset-before" page:_currentIndex scroll:scrollView];
3253
3920
  [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, logicalOffset - original.top)
3254
3921
  animated:NO];
3255
- [self offsetDiag:@"release-original-offset-after" page:_currentIndex scroll:scrollView];
3256
3922
  [_originalInsets removeObjectForKey:scrollView];
3257
3923
  [_appliedTopInsets removeObjectForKey:scrollView];
3258
3924
  [_originalIndicatorInsets removeObjectForKey:scrollView];
@@ -3283,7 +3949,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3283
3949
  {
3284
3950
  // Smooth pages retain their complete offset while replacement identity and
3285
3951
  // layout settle. Legacy pages retain the historical header-only policy.
3286
- [self offsetDiag:restore ? @"restore-enter" : @"insets-enter" page:pageIndex scroll:scrollView];
3287
3952
  NSString *pageKey = [self pageKeyForIndex:pageIndex];
3288
3953
  RNCCollapsiblePagerReleasedScrollState *released = _releasedPageScrollStates[pageKey];
3289
3954
  BOOL deferredReleasedIdentity = NO;
@@ -3329,13 +3994,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3329
3994
  deferredReleasedIdentity = now - released.pendingFallbackDetectedAt <
3330
3995
  RNCCollapsiblePagerFallbackDebounceInterval;
3331
3996
  }
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
3997
  if (!deferredReleasedIdentity) {
3336
- [self offsetDiag:@"released-identity-cache-before" page:pageIndex scroll:scrollView];
3337
3998
  if (!sameList && !_nativeSmoothHeaderScrollEnabled) _pageOffsets[pageKey] = @(_headerOffset);
3338
- [self offsetDiag:@"released-identity-cache-after" page:pageIndex scroll:scrollView];
3339
3999
  [_releasedPageScrollStates removeObjectForKey:pageKey];
3340
4000
  }
3341
4001
  }
@@ -3360,22 +4020,24 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3360
4020
  // suppress its later vertical movement while the pager is still pending.
3361
4021
  scrollView.directionalLockEnabled = _nativeSmoothHeaderScrollEnabled
3362
4022
  ? NO : [_originalDirectionalLockEnabled objectForKey:scrollView].boolValue;
3363
- [self updateNativeScrollerViewport:scrollView stickyHeight:_stickyHeaderHeight];
4023
+ [self updateNativeScrollerViewport:scrollView stickyHeight:_stickyHeaderHeight + self.effectiveTopOverlayHeight];
3364
4024
  [self updateRefreshControlForScrollView:scrollView];
3365
4025
  CGFloat previousTopInset = scrollView.contentInset.top;
3366
4026
  CGFloat previousLogicalOffset = scrollView.contentOffset.y + previousTopInset;
4027
+ // Preserve UIKit's rebound after refresh while the content is beyond its top inset.
4028
+ BOOL wasReturningFromPull = scrollView.contentOffset.y < -scrollView.adjustedContentInset.top - 0.5;
3367
4029
  NSNumber *appliedTop = [_appliedTopInsets objectForKey:scrollView];
3368
4030
  // UIRefreshControl owns its temporary top inset, including while ending.
3369
4031
  CGFloat refreshInset = appliedTop != nil && scrollView.refreshControl != nil
3370
4032
  ? MAX(0, previousTopInset - appliedTop.doubleValue) : 0;
3371
4033
  UIEdgeInsets next = original;
3372
- next.top += _headerHeight + _stickyHeaderHeight;
4034
+ next.top += self.effectiveTopOverlayHeight + _headerHeight + _stickyHeaderHeight;
3373
4035
  CGFloat pagerTopInset = next.top;
3374
4036
  next.top += refreshInset;
3375
4037
  [_appliedTopInsets setObject:@(pagerTopInset) forKey:scrollView];
3376
4038
  // Short pages still need enough range to collapse the header; long pages
3377
4039
  // must end at the last row rather than leave a header-sized blank footer.
3378
- next.bottom += MAX(0, scrollView.bounds.size.height - _stickyHeaderHeight
4040
+ next.bottom += MAX(0, scrollView.bounds.size.height - _stickyHeaderHeight - self.effectiveTopOverlayHeight
3379
4041
  - original.top - original.bottom - scrollView.contentSize.height);
3380
4042
  if (UIEdgeInsetsEqualToEdgeInsets(scrollView.contentInset, next) && !restore) {
3381
4043
  // UICollectionView may report contentSize during a bounce without changing
@@ -3385,9 +4047,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3385
4047
  }
3386
4048
  BOOL previousSyntheticRestore = _syntheticPageRestore;
3387
4049
  if (_nativeSmoothHeaderScrollEnabled || [_alignedBodyKeys containsObject:pageKey]) _syntheticPageRestore = YES;
3388
- [self offsetDiag:@"inset-assignment-before" page:pageIndex scroll:scrollView];
3389
4050
  scrollView.contentInset = next;
3390
- [self offsetDiag:@"inset-assignment-after" page:pageIndex scroll:scrollView];
3391
4051
  UIEdgeInsets indicatorInsets = scrollView.verticalScrollIndicatorInsets;
3392
4052
  indicatorInsets.top = pagerTopInset;
3393
4053
  scrollView.verticalScrollIndicatorInsets = indicatorInsets;
@@ -3419,9 +4079,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3419
4079
  } else {
3420
4080
  restoredLogicalOffset = deferredReleasedIdentity || saved == nil ? _headerOffset : saved.doubleValue;
3421
4081
  }
3422
- #if DEBUG
3423
- [self offsetDiag:[NSString stringWithFormat:@"restore-candidate-saved=%.4f-headerAlignedLogical=%.4f", saved.doubleValue, restoredLogicalOffset] page:pageIndex scroll:scrollView];
3424
- #endif
3425
4082
  intendedLogicalOffset = restoredLogicalOffset;
3426
4083
  // Cancellation restores the visible source, while retaining its unreachable reading intent.
3427
4084
  if (_nativeSmoothHeaderScrollEnabled && _headerOffset >= _headerHeight &&
@@ -3434,9 +4091,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3434
4091
  }
3435
4092
  // An inactive list may shrink after its offset observer was detached.
3436
4093
  // 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
4094
  targetOffset = RNCLimitRestoredScrollOffset(
3441
4095
  targetOffset,
3442
4096
  scrollView.contentSize.height,
@@ -3445,13 +4099,17 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3445
4099
  next.bottom
3446
4100
  );
3447
4101
  }
3448
- #if DEBUG
3449
- [self offsetDiag:[NSString stringWithFormat:@"restore-final-target=%.4f-restore=%d", targetOffset, restore] page:pageIndex scroll:scrollView];
3450
- #endif
3451
4102
  // A bottom-inset change must not cancel UIKit's refresh or bounce settlement.
3452
- if (restore || previousTopInset != next.top) {
4103
+ if (restore || (previousTopInset != next.top && !wasReturningFromPull)) {
3453
4104
  [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, targetOffset) animated:NO];
3454
4105
  }
4106
+ if (_applyingTopOverlayInsetChange && !restore && wasReturningFromPull &&
4107
+ previousTopInset != next.top && scrollView == _observedScrollView) {
4108
+ _pendingTopInsetReboundScrollView = scrollView;
4109
+ _pendingTopInsetReboundPageKey = [pageKey copy];
4110
+ _pendingTopInsetReboundGeneration = _generation;
4111
+ [self scheduleTopInsetReboundCheck];
4112
+ }
3455
4113
  if (_nativeSmoothHeaderScrollEnabled && restore && targetOffset + next.top + 0.5 < intendedLogicalOffset) {
3456
4114
  [_pendingPageRestoreKeys addObject:pageKey];
3457
4115
  }
@@ -3461,7 +4119,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3461
4119
  if (!_nativeSmoothHeaderScrollEnabled) [self updateHeaderForScrollView:scrollView];
3462
4120
  else [self applyHeaderOffset];
3463
4121
  }
3464
- [self offsetDiag:@"restore-post" page:pageIndex scroll:scrollView];
3465
4122
  }
3466
4123
 
3467
4124
  - (void)updateNativeScrollerViewport:(UIScrollView *)scrollView stickyHeight:(CGFloat)height
@@ -3478,6 +4135,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3478
4135
  _observedPageKey == nil || ![_observedPageKey isEqualToString:self.currentPageKey] ||
3479
4136
  _observedPageIndex != _currentIndex || _observedPageGeneration != _generation ||
3480
4137
  _transitioning || _isPagerDragging || _transitionPageOffsets != nil || _syntheticPageRestore) return;
4138
+ [self cancelTopInsetReboundCheck];
3481
4139
  NSInteger phase = [notification.userInfo[@"phase"] integerValue];
3482
4140
  [self updateHeaderSettledStateForceBusy:NO];
3483
4141
  if (phase == 1) _headerCommandOwner = scrollView;
@@ -3510,7 +4168,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3510
4168
  _headerOffset = 0;
3511
4169
  [self applyHeaderOffset];
3512
4170
  }
3513
- [self offsetDiag:@"list-command-body-takeover" page:_currentIndex scroll:scrollView];
3514
4171
  }
3515
4172
 
3516
4173
  - (void)nativeScrollerWillReceiveScrollCommand:(RNCNativeScrollerComponentView *)scroller
@@ -3522,12 +4179,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3522
4179
  _observedPageIndex != _currentIndex || _currentIndex < 0 || _currentIndex >= _pageControllers.count ||
3523
4180
  _observedPageGeneration != _generation || _transitioning || _isPagerDragging ||
3524
4181
  _transitionPageOffsets != nil || _syntheticPageRestore) return;
4182
+ [self cancelTopInsetReboundCheck];
3525
4183
  if (!_dispatchingPagerTop) {
3526
4184
  [self cancelPagerTop];
3527
4185
  [_alignedBodyKeys removeObject:ownerKey];
3528
4186
  }
3529
4187
  if (!_nativeSmoothHeaderScrollEnabled) return;
3530
- [self offsetDiag:@"command-pending-cancel-before" page:_observedPageIndex scroll:scrollView];
3531
4188
  [_pendingPageRestoreKeys removeObject:ownerKey];
3532
4189
  _currentLogicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
3533
4190
  [_commandBodyKeys removeObject:ownerKey];
@@ -3536,7 +4193,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3536
4193
  [_displayBodyOverrides removeObjectForKey:ownerKey];
3537
4194
  _pageOffsets[ownerKey] = @(MAX(_currentLogicalOffset - _headerHeight, 0));
3538
4195
  [self updateHeaderForScrollView:scrollView];
3539
- [self offsetDiag:@"command-pending-cancel-after" page:_observedPageIndex scroll:scrollView];
3540
4196
  }
3541
4197
 
3542
4198
 
@@ -3598,6 +4254,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3598
4254
  return;
3599
4255
  }
3600
4256
  if (context == RNCCollapsiblePagerContentOffsetContext && object == _observedScrollView) {
4257
+ if (_pendingTopInsetReboundScrollView == object &&
4258
+ ([keyPath isEqualToString:@"contentOffset"] || [keyPath isEqualToString:@"contentInset"])) {
4259
+ if (((UIScrollView *)object).panGestureRecognizer.state == UIGestureRecognizerStateChanged) {
4260
+ [self cancelTopInsetReboundCheck];
4261
+ } else {
4262
+ [self scheduleTopInsetReboundCheck];
4263
+ }
4264
+ }
3601
4265
  if (![change[NSKeyValueChangeOldKey] isEqual:change[NSKeyValueChangeNewKey]]) {
3602
4266
  [self updateHeaderSettledStateForceBusy:YES];
3603
4267
  }
@@ -3662,18 +4326,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3662
4326
  }
3663
4327
  UIScrollView *scrollView = (UIScrollView *)object;
3664
4328
  _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
4329
+ if (_correctingTopInsetRebound) return;
3669
4330
  if ([_alignedBodyKeys containsObject:_observedPageKey] ||
3670
4331
  (_nativeSmoothHeaderScrollEnabled && scrollView.panGestureRecognizer.state != UIGestureRecognizerStateChanged &&
3671
4332
  !scrollView.isDecelerating && ![_commandBodyKeys containsObject:_observedPageKey])) return;
3672
4333
  _pageOffsets[_nativeSmoothHeaderScrollEnabled ? _observedPageKey : self.currentPageKey] =
3673
4334
  @(_nativeSmoothHeaderScrollEnabled ? MAX(_currentLogicalOffset - _headerHeight, 0) : _currentLogicalOffset);
3674
- #if DEBUG
3675
- if (offsetDiagChanged) [self offsetDiag:@"kvo-cache-after" page:_currentIndex scroll:scrollView];
3676
- #endif
3677
4335
  [self updateHeaderForScrollView:scrollView];
3678
4336
  return;
3679
4337
  }
@@ -3714,17 +4372,22 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3714
4372
  ? MAX(0, scrollView.contentInset.top - appliedTop.doubleValue) : 0;
3715
4373
  CGFloat pinnedTranslation = MAX(0, _currentLogicalOffset - _headerHeight - refreshInset);
3716
4374
  CGAffineTransform stickyTransform = CGAffineTransformMakeTranslation(0, pinnedTranslation);
3717
- _stickyHeaderView.transform = stickyTransform;
3718
- _nativeTabBarView.transform = stickyTransform;
3719
- _nativeSubHeaderView.transform = stickyTransform;
4375
+ [self updatePinnedStickyHeader];
4376
+ _stickyHeaderView.transform = _stickyHeaderIsPinned ? CGAffineTransformIdentity : stickyTransform;
4377
+ _nativeTabBarView.transform = _stickyHeaderIsPinned ? CGAffineTransformIdentity : stickyTransform;
4378
+ _nativeSubHeaderView.transform = _stickyHeaderIsPinned ? CGAffineTransformIdentity : stickyTransform;
4379
+ [self updateScrollEdgeContentMasks];
3720
4380
  return;
3721
4381
  }
4382
+ [self updatePinnedStickyHeader];
3722
4383
  CGAffineTransform collapseTransform = CGAffineTransformMakeTranslation(0, -_headerOffset);
3723
4384
  _sharedHeaderHostView.transform = collapseTransform;
3724
4385
  _headerView.transform = CGAffineTransformIdentity;
3725
4386
  _stickyHeaderView.transform = CGAffineTransformIdentity;
3726
4387
  _nativeTabBarView.transform = CGAffineTransformIdentity;
3727
4388
  _nativeSubHeaderView.transform = CGAffineTransformIdentity;
4389
+ [self updateTopScrollEdgeForScrollView:_observedScrollView];
4390
+ [self updateScrollEdgeContentMasks];
3728
4391
  }
3729
4392
 
3730
4393
  #pragma mark - Pager navigation
@@ -3736,7 +4399,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3736
4399
 
3737
4400
  - (void)goTo:(NSInteger)index animated:(BOOL)animated
3738
4401
  {
3739
- [self offsetDiag:@"go-to" page:index scroll:_observedScrollView];
4402
+ [self cancelTopInsetReboundCheck];
3740
4403
  if (_isBeingRecycled || index < 0 || index >= _pageControllers.count) return;
3741
4404
  _hasReceivedPageCommand = YES;
3742
4405
  _hasAppliedInitialPage = YES;
@@ -3867,6 +4530,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3867
4530
  if (!self->_transitioning && !self->_needsSlotRebuild) {
3868
4531
  [self attachScrollObserverForCurrentPage];
3869
4532
  }
4533
+ [self updateScrollEdgeContentMasks];
3870
4534
  });
3871
4535
  }
3872
4536
 
@@ -3918,7 +4582,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3918
4582
  - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
3919
4583
  {
3920
4584
  [self updateHeaderSettledStateForceBusy:YES];
3921
- [self offsetDiag:@"pager-drag-begin" page:_currentIndex scroll:_observedScrollView];
3922
4585
  if (_nativeSmoothHeaderScrollEnabled && scrollView == _pagerScrollView) {
3923
4586
  [_contentPressCancellationGesture cancelPressForPan:scrollView.panGestureRecognizer];
3924
4587
  }
@@ -4075,6 +4738,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4075
4738
  CGFloat offset = progress - position;
4076
4739
  position = MAX(0, MIN(position, (NSInteger)_pageControllers.count - 1));
4077
4740
  [self updatePageStateTransitionProgress:progress];
4741
+ [self updateScrollEdgeContentMasks];
4078
4742
  [_nativeTabBarView setProgress:progress];
4079
4743
  const auto emitter = [self eventEmitter];
4080
4744
  if (emitter) {
@@ -4093,6 +4757,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4093
4757
  tabProgress = _currentIndex + (_destinationIndex - _currentIndex) * transitionProgress;
4094
4758
  }
4095
4759
  [self updatePageStateTransitionProgress:tabProgress];
4760
+ [self updateScrollEdgeContentMasks];
4096
4761
  [_nativeTabBarView setProgress:tabProgress];
4097
4762
  const auto emitter = [self eventEmitter];
4098
4763
  if (emitter) {
@@ -4110,7 +4775,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4110
4775
 
4111
4776
  - (void)emitPageSelected:(NSInteger)position
4112
4777
  {
4113
- [self offsetDiag:@"selection-complete" page:position scroll:_observedScrollView];
4114
4778
  const auto emitter = [self eventEmitter];
4115
4779
  if (emitter) emitter->onPageSelected({.position = (double)position});
4116
4780
  }
@@ -4255,12 +4919,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4255
4919
  _currentLogicalOffset = logical;
4256
4920
  _headerOffset = _pagerTopLogicalTarget;
4257
4921
  [self applyHeaderOffset];
4258
- [self offsetDiag:[NSString stringWithFormat:@"pager-top-complete-token=%lu-target=%.4f", (unsigned long)_pagerTopToken, _pagerTopLogicalTarget] page:_currentIndex scroll:scrollView];
4259
4922
  [self cancelPagerTop];
4260
4923
  }
4261
4924
 
4262
4925
  - (void)scrollToTop:(NSInteger)target animated:(BOOL)animated
4263
4926
  {
4927
+ [self cancelTopInsetReboundCheck];
4264
4928
  UIScrollView *scrollView = _observedScrollView;
4265
4929
  NSString *key = _observedPageKey;
4266
4930
  if ((target != 0 && target != 1) || _isBeingRecycled || _transitioning || _isPagerDragging ||
@@ -4292,7 +4956,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4292
4956
  [self applyHeaderOffset];
4293
4957
  CGFloat physicalTarget = RNCLimitRestoredScrollOffset(logicalTarget - scrollView.contentInset.top,
4294
4958
  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
4959
  _dispatchingPagerTop = YES;
4297
4960
  [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, physicalTarget) animated:animated];
4298
4961
  _dispatchingPagerTop = NO;
@@ -4357,6 +5020,22 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4357
5020
  : velocity;
4358
5021
  BOOL hasMotion = MAX(fabs(intent.x), fabs(intent.y)) >= 1;
4359
5022
  BOOL horizontal = hasMotion && fabs(intent.x) > fabs(intent.y);
5023
+ if (_sharedHeaderOuterPagerGesture.protectsNestedBody) {
5024
+ NSInteger pageCount = _pageControllers.count;
5025
+ BOOL validPage = _currentIndex >= 0 && _currentIndex < pageCount;
5026
+ NSInteger firstPageIndex = self.isLtrLayout ? 0 : pageCount - 1;
5027
+ NSInteger lastPageIndex = self.isLtrLayout ? pageCount - 1 : 0;
5028
+ BOOL outward = validPage &&
5029
+ ((_currentIndex == firstPageIndex && intent.x > 0) ||
5030
+ (_currentIndex == lastPageIndex && intent.x < 0));
5031
+ UIScrollView *outer = RNCNestedOuterReactPagerScrollView(self);
5032
+ BOOL protectInnerPage = horizontal && validPage && pageCount > 1 &&
5033
+ _nestedScrollEnabled && _scrollEnabled && _pagerScrollView.scrollEnabled &&
5034
+ _pagerScrollView.panGestureRecognizer.enabled && outer != nil && outer.scrollEnabled &&
5035
+ [_sharedHeaderOuterPagerGesture.blockedPagerGestures
5036
+ containsObject:outer.panGestureRecognizer] && !outward;
5037
+ return protectInnerPage;
5038
+ }
4360
5039
  RNCCollapsiblePagerLog([NSString stringWithFormat:
4361
5040
  @"header-axis role=outer-pager-block dx=%.2f dy=%.2f velocity=(%.2f,%.2f) result=%d outer-pagers=%lu",
4362
5041
  intent.x,
@@ -4396,24 +5075,21 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4396
5075
  return vertical;
4397
5076
  }
4398
5077
  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];
5078
+ UIPanGestureRecognizer *pan = [gestureRecognizer isKindOfClass:UIPanGestureRecognizer.class]
5079
+ ? (UIPanGestureRecognizer *)gestureRecognizer : nil;
5080
+ CGPoint velocity = pan != nil ? [pan velocityInView:self] : CGPointZero;
4404
5081
  NSInteger pageCount = _pageControllers.count;
4405
- if (pageCount == 0 || _currentIndex < 0 || _currentIndex >= pageCount) return NO;
4406
-
4407
5082
  CGFloat absVelocityX = fabs(velocity.x);
4408
5083
  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;
5084
+ NSInteger firstPageIndex = self.isLtrLayout ? 0 : pageCount - 1;
5085
+ NSInteger lastPageIndex = self.isLtrLayout ? pageCount - 1 : 0;
5086
+ BOOL validPage = pageCount > 0 && _currentIndex >= 0 && _currentIndex < pageCount;
5087
+ BOOL horizontal = absVelocityX > absVelocityY && absVelocityX >= 1.0f;
5088
+ BOOL outward = validPage &&
5089
+ ((_currentIndex == firstPageIndex && velocity.x > 0) ||
5090
+ (_currentIndex == lastPageIndex && velocity.x < 0));
5091
+ BOOL shouldBegin = _nestedScrollEnabled && pan != nil && horizontal && outward;
5092
+ return shouldBegin;
4417
5093
  }
4418
5094
 
4419
5095
  - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
@@ -4423,13 +5099,29 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4423
5099
  _sharedHeaderPressCancellationGestureHost == nil) return NO;
4424
5100
  UIView *touchView = touch.view;
4425
5101
  BOOL beginsInSharedHeader = touchView == _sharedHeaderHostView ||
4426
- [touchView isDescendantOfView:_sharedHeaderHostView];
4427
- if (!beginsInSharedHeader) return NO;
5102
+ [touchView isDescendantOfView:_sharedHeaderHostView] ||
5103
+ touchView == _pinnedStickyHostView ||
5104
+ [touchView isDescendantOfView:_pinnedStickyHostView];
5105
+ if (_sharedHeaderOuterPagerGesture.trackedTouch != nil &&
5106
+ _sharedHeaderOuterPagerGesture.trackedTouch != touch) return NO;
5107
+ UIScrollView *outer = RNCNestedOuterReactPagerScrollView(self);
5108
+ BOOL beginsInNestedBody = !beginsInSharedHeader && _nestedScrollEnabled &&
5109
+ _scrollEnabled && _pagerScrollView.scrollEnabled &&
5110
+ _pagerScrollView.panGestureRecognizer.enabled && _pageControllers.count > 1 &&
5111
+ outer != nil && outer.scrollEnabled &&
5112
+ [_sharedHeaderOuterPagerGesture.blockedPagerGestures containsObject:outer.panGestureRecognizer] &&
5113
+ (touchView == _pagerScrollView || [touchView isDescendantOfView:_pagerScrollView]);
5114
+ if (!beginsInSharedHeader && !beginsInNestedBody) return NO;
5115
+
5116
+ _sharedHeaderOuterPagerGesture.trackedTouch = touch;
5117
+ _sharedHeaderOuterPagerGesture.protectsNestedBody = beginsInNestedBody;
5118
+ _sharedHeaderOuterPagerGesture.cancelsTouchesInView = !beginsInNestedBody;
4428
5119
 
4429
5120
  RNCCollapsiblePagerLog([NSString stringWithFormat:
4430
- @"outer-pager-touch target=%@ outer-pagers=%lu",
5121
+ @"outer-pager-touch target=%@ outer-pagers=%lu nested-body=%d",
4431
5122
  touchView == nil ? @"none" : NSStringFromClass(touchView.class),
4432
- (unsigned long)_sharedHeaderOuterPagerGesture.blockedPagerGestures.count]);
5123
+ (unsigned long)_sharedHeaderOuterPagerGesture.blockedPagerGestures.count,
5124
+ beginsInNestedBody]);
4433
5125
  return YES;
4434
5126
  }
4435
5127
  if (gestureRecognizer == _contentPressCancellationGesture) {
@@ -4438,14 +5130,17 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4438
5130
  return _nativeSmoothHeaderScrollEnabled && !_isBeingRecycled &&
4439
5131
  _contentPressCancellationGesture.view != nil &&
4440
5132
  [touchView isDescendantOfView:_pagerScrollView] &&
4441
- ![touchView isDescendantOfView:_sharedHeaderHostView];
5133
+ ![touchView isDescendantOfView:_sharedHeaderHostView] &&
5134
+ ![touchView isDescendantOfView:_pinnedStickyHostView];
4442
5135
  }
4443
5136
  if (gestureRecognizer == _sharedHeaderPressCancellationGesture) {
4444
5137
  if (!_nativeSmoothHeaderScrollEnabled || _isBeingRecycled ||
4445
5138
  _sharedHeaderPressCancellationGestureHost == nil) return NO;
4446
5139
  UIView *touchView = touch.view;
4447
5140
  BOOL beginsInSharedHeader = touchView == _sharedHeaderHostView ||
4448
- [touchView isDescendantOfView:_sharedHeaderHostView];
5141
+ [touchView isDescendantOfView:_sharedHeaderHostView] ||
5142
+ touchView == _pinnedStickyHostView ||
5143
+ [touchView isDescendantOfView:_pinnedStickyHostView];
4449
5144
  if (!beginsInSharedHeader) return NO;
4450
5145
 
4451
5146
  RNCCollapsiblePagerLog([NSString stringWithFormat:
@@ -4456,7 +5151,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4456
5151
  if (gestureRecognizer == _verticalPagerGesture) {
4457
5152
  UIView *touchView = touch.view;
4458
5153
  return touchView != _sharedHeaderHostView &&
4459
- ![touchView isDescendantOfView:_sharedHeaderHostView];
5154
+ ![touchView isDescendantOfView:_sharedHeaderHostView] &&
5155
+ touchView != _pinnedStickyHostView &&
5156
+ ![touchView isDescendantOfView:_pinnedStickyHostView];
4460
5157
  }
4461
5158
  // A new touch can cancel UIKit's animation before its first frame. Queue a
4462
5159
  // non-animated settle for the next safe transition boundary instead of