@onekeyfe/react-native-pager-view 3.0.179-alpha.302 → 3.0.179-alpha.304

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,320 @@ 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
+ _externalOuterPager == nil ||
2054
+ (_externalOuterPageActive && !_externalOuterTransitioning);
2055
+ BOOL hasOverlay = _topScrollEdgeHard && self.hasFixedTopOverlay &&
2056
+ externalOwnerReady && self.window != nil;
2057
+ BOOL hasSticky = _topScrollEdgeHard && _stickyHeaderView != nil &&
2058
+ externalOwnerReady && self.window != nil;
2059
+ BOOL enabled = (hasOverlay || hasSticky) && scrollView != nil;
2060
+ BOOL topHostChanged = (_topEdgeInteraction != nil) != hasOverlay ||
2061
+ (hasOverlay && _topEdgeInteractionHost != _topOverlayHostView);
2062
+ if (_topEdgeScrollView != nil && (!enabled || _topEdgeScrollView != scrollView ||
2063
+ topHostChanged)) {
2064
+ [_topEdgeInteractionHost removeInteraction:_topEdgeInteraction];
2065
+ _topEdgeInteraction.scrollView = nil;
2066
+ [_stickyEdgeInteractionHost removeInteraction:_stickyEdgeInteraction];
2067
+ _stickyEdgeInteraction.scrollView = nil;
2068
+ [_topEdgeMarker removeFromSuperview];
2069
+ [_stickyEdgeMarker removeFromSuperview];
2070
+ _topEdgeScrollView.topEdgeEffect.style = _originalTopEdgeStyle;
2071
+ _topEdgeInteraction = nil;
2072
+ _stickyEdgeInteraction = nil;
2073
+ _topEdgeMarker = nil;
2074
+ _stickyEdgeMarker = nil;
2075
+ _topEdgeInteractionHost = nil;
2076
+ _stickyEdgeInteractionHost = nil;
2077
+ _topEdgeScrollView = nil;
2078
+ _originalTopEdgeStyle = nil;
2079
+ }
2080
+ if (enabled && _topEdgeScrollView == nil) {
2081
+ _originalTopEdgeStyle = scrollView.topEdgeEffect.style;
2082
+ _topEdgeScrollView = scrollView;
2083
+ scrollView.topEdgeEffect.style = UIScrollEdgeEffectStyle.hardStyle;
2084
+ }
2085
+ if (hasOverlay && enabled && _topEdgeInteraction == nil) {
2086
+ // UIKit needs a native element to shape the edge effect around React-drawn content.
2087
+ _topEdgeMarker = [UILabel new];
2088
+ _topEdgeMarker.frame = _topOverlayHostView.bounds;
2089
+ _topEdgeMarker.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
2090
+ _topEdgeMarker.text = @" ";
2091
+ _topEdgeMarker.textColor = UIColor.clearColor;
2092
+ _topEdgeMarker.backgroundColor = UIColor.clearColor;
2093
+ _topEdgeMarker.userInteractionEnabled = NO;
2094
+ _topEdgeMarker.isAccessibilityElement = NO;
2095
+ _topEdgeMarker.accessibilityElementsHidden = YES;
2096
+ [_topOverlayHostView addSubview:_topEdgeMarker];
2097
+ _topEdgeInteraction = [UIScrollEdgeElementContainerInteraction new];
2098
+ _topEdgeInteraction.edge = UIRectEdgeTop;
2099
+ _topEdgeInteraction.scrollView = scrollView;
2100
+ [_topOverlayHostView addInteraction:_topEdgeInteraction];
2101
+ _topEdgeInteractionHost = _topOverlayHostView;
2102
+ }
2103
+ BOOL legacyStickyPinned = !_nativeSmoothHeaderScrollEnabled &&
2104
+ _stickyHeaderView != nil && _headerOffset >= _headerHeight - 0.5;
2105
+ BOOL stickyEnabled = enabled && (_stickyHeaderIsPinned || legacyStickyPinned);
2106
+ UIView *stickyInteractionHost = _stickyHeaderIsPinned
2107
+ ? _pinnedStickyHostView : _stickyHeaderView;
2108
+ if (_stickyEdgeInteractionHost != nil &&
2109
+ (!stickyEnabled || _stickyEdgeInteractionHost != stickyInteractionHost)) {
2110
+ [_stickyEdgeInteractionHost removeInteraction:_stickyEdgeInteraction];
2111
+ _stickyEdgeInteraction.scrollView = nil;
2112
+ [_stickyEdgeMarker removeFromSuperview];
2113
+ _stickyEdgeMarker = nil;
2114
+ _stickyEdgeInteraction = nil;
2115
+ _stickyEdgeInteractionHost = nil;
2116
+ }
2117
+ if (stickyEnabled && _stickyEdgeInteraction == nil) {
2118
+ _stickyEdgeMarker = [UILabel new];
2119
+ _stickyEdgeMarker.frame = stickyInteractionHost.bounds;
2120
+ _stickyEdgeMarker.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
2121
+ _stickyEdgeMarker.text = @" ";
2122
+ _stickyEdgeMarker.textColor = UIColor.clearColor;
2123
+ _stickyEdgeMarker.backgroundColor = UIColor.clearColor;
2124
+ _stickyEdgeMarker.userInteractionEnabled = NO;
2125
+ _stickyEdgeMarker.isAccessibilityElement = NO;
2126
+ _stickyEdgeMarker.accessibilityElementsHidden = YES;
2127
+ [stickyInteractionHost addSubview:_stickyEdgeMarker];
2128
+ _stickyEdgeInteraction = [UIScrollEdgeElementContainerInteraction new];
2129
+ _stickyEdgeInteraction.edge = UIRectEdgeTop;
2130
+ _stickyEdgeInteraction.scrollView = scrollView;
2131
+ [stickyInteractionHost addInteraction:_stickyEdgeInteraction];
2132
+ _stickyEdgeInteractionHost = stickyInteractionHost;
2133
+ }
2134
+ [self updateScrollEdgeContentMasks];
2135
+ }
2136
+ }
2137
+
1715
2138
  - (void)dealloc
1716
2139
  {
2140
+ [self updateTopScrollEdgeForScrollView:nil];
1717
2141
  [self cancelHeaderSettledCheck];
1718
2142
  [[NSNotificationCenter defaultCenter] removeObserver:self name:@"OneKeyNativeListScrollCommandAccepted" object:nil];
1719
2143
  for (UIScrollView *scrollView in _originalInsets.keyEnumerator) {
@@ -1766,6 +2190,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1766
2190
  _hasPendingDirectNativePagerChange = NO;
1767
2191
  if (_directNativePagerEnabled == enabled) return;
1768
2192
 
2193
+ [self restoreScrollEdgeContentMasks];
1769
2194
  [self detachScrollObserver];
1770
2195
  [self restoreSharedHeadersToContainer];
1771
2196
  if (_blockerGesture != nil) {
@@ -1841,8 +2266,33 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1841
2266
 
1842
2267
  #pragma mark - Smooth shared header ownership
1843
2268
 
2269
+ - (void)restorePinnedStickyHeader
2270
+ {
2271
+ if (!_stickyHeaderIsPinned) return;
2272
+ _stickyHeaderIsPinned = NO;
2273
+ _stickyHeaderView.transform = CGAffineTransformIdentity;
2274
+ _nativeTabBarView.transform = CGAffineTransformIdentity;
2275
+ _nativeSubHeaderView.transform = CGAffineTransformIdentity;
2276
+ [_stickyHeaderView removeFromSuperview];
2277
+ [_nativeTabBarView removeFromSuperview];
2278
+ [_nativeSubHeaderView removeFromSuperview];
2279
+ if (_stickyHeaderView != nil) [_sharedHeaderHostView addSubview:_stickyHeaderView];
2280
+ [_sharedHeaderHostView addSubview:_nativeTabBarView];
2281
+ [_sharedHeaderHostView addSubview:_nativeSubHeaderView];
2282
+ _pinnedStickyHostView.hidden = YES;
2283
+ _pinnedStickyHostView.layer.zPosition = 0;
2284
+ [_containerView addSubview:_pinnedStickyHostView];
2285
+ if ([_pagerScrollView isKindOfClass:RNCCollapsiblePagerDirectScrollView.class]) {
2286
+ ((RNCCollapsiblePagerDirectScrollView *)_pagerScrollView).excludedHeaderView = _sharedHeaderHostView;
2287
+ }
2288
+ [_nativeTabBarView updateAncestorGesturePrecedence];
2289
+ [_nativeSubHeaderView updateAncestorGesturePrecedence];
2290
+ }
2291
+
1844
2292
  - (void)restoreSharedHeadersToContainer
1845
2293
  {
2294
+ [self restorePinnedStickyHeader];
2295
+ [self updateTopScrollEdgeForScrollView:_observedScrollView];
1846
2296
  _sharedHeaderScrollView = nil;
1847
2297
  _sharedHeadersLiftedForPagerTransition = NO;
1848
2298
  _sharedHeaderHostView.layer.zPosition = 0;
@@ -1850,11 +2300,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1850
2300
  [_containerView addSubview:_sharedHeaderHostView];
1851
2301
  }
1852
2302
  [self layoutSharedHeaders];
2303
+ if (self.hasFixedTopOverlay) {
2304
+ [_containerView bringSubviewToFront:_topOverlayHostView];
2305
+ }
1853
2306
  }
1854
2307
 
1855
2308
  - (void)attachSharedHeadersToScrollView:(UIScrollView *)scrollView
1856
2309
  {
1857
2310
  if (!_nativeSmoothHeaderScrollEnabled || _transitioning || _isPagerDragging ||
2311
+ _externalOuterTransitioning ||
1858
2312
  scrollView == nil || _isBeingRecycled) return;
1859
2313
  if ((UIView *)scrollView == _sharedHeaderHostView ||
1860
2314
  [scrollView isDescendantOfView:_sharedHeaderHostView]) {
@@ -1866,13 +2320,16 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1866
2320
  }
1867
2321
  if (_sharedHeaderScrollView == scrollView &&
1868
2322
  _sharedHeaderHostView.superview == scrollView) {
2323
+ _sharedHeaderHostView.layer.zPosition =
2324
+ _topScrollEdgeHard && (self.hasFixedTopOverlay || _stickyHeaderView != nil) ? 0 : 1000;
1869
2325
  [self layoutSharedHeaders];
1870
2326
  return;
1871
2327
  }
1872
2328
 
1873
2329
  _sharedHeaderScrollView = scrollView;
1874
2330
  _sharedHeadersLiftedForPagerTransition = NO;
1875
- _sharedHeaderHostView.layer.zPosition = 1000;
2331
+ _sharedHeaderHostView.layer.zPosition =
2332
+ _topScrollEdgeHard && (self.hasFixedTopOverlay || _stickyHeaderView != nil) ? 0 : 1000;
1876
2333
  if (_sharedHeaderHostView.superview != scrollView) {
1877
2334
  [scrollView addSubview:_sharedHeaderHostView];
1878
2335
  }
@@ -1919,56 +2376,114 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1919
2376
 
1920
2377
  _sharedHeaderHostView.frame = CGRectMake(
1921
2378
  0,
1922
- attachedToList ? -totalHeight : 0,
2379
+ attachedToList ? -totalHeight : self.effectiveTopOverlayHeight,
1923
2380
  width,
1924
2381
  totalHeight
1925
2382
  );
1926
2383
  CGFloat stickyY = _headerHeight;
1927
2384
  _headerView.frame = CGRectMake(0, 0, width, _headerHeight);
1928
- _stickyHeaderView.frame = CGRectMake(0, stickyY, width, _stickyHeaderHeight);
2385
+ CGPoint pinnedOrigin = _stickyHeaderIsPinned
2386
+ ? [_sharedHeaderScrollView convertPoint:CGPointMake(0, self.effectiveTopOverlayHeight) fromView:_containerView]
2387
+ : CGPointMake(0, self.effectiveTopOverlayHeight);
2388
+ _pinnedStickyHostView.frame = CGRectMake(
2389
+ pinnedOrigin.x, pinnedOrigin.y, width, _stickyHeaderHeight);
2390
+ CGFloat stickyChildY = _stickyHeaderIsPinned ? 0 : stickyY;
2391
+ _stickyHeaderView.frame = CGRectMake(0, stickyChildY, width, _stickyHeaderHeight);
1929
2392
  CGFloat nativeTabBarVisibleHeight = _nativeTabBarView.hidden
1930
2393
  ? 0
1931
2394
  : MIN(_stickyHeaderHeight, _nativeTabBarHeight);
1932
2395
  _nativeTabBarView.frame = CGRectMake(
1933
2396
  0,
1934
- stickyY,
2397
+ stickyChildY,
1935
2398
  width,
1936
2399
  nativeTabBarVisibleHeight
1937
2400
  );
1938
2401
  _nativeSubHeaderView.frame = CGRectMake(
1939
2402
  0,
1940
- stickyY + nativeTabBarVisibleHeight,
2403
+ stickyChildY + nativeTabBarVisibleHeight,
1941
2404
  width,
1942
2405
  MIN(
1943
2406
  MAX(0, _stickyHeaderHeight - nativeTabBarVisibleHeight),
1944
2407
  _nativeSubHeaderHeight
1945
2408
  )
1946
2409
  );
2410
+ _topOverlayHostView.frame = CGRectMake(0, 0, width, self.effectiveTopOverlayHeight);
2411
+ _topOverlayView.frame = _topOverlayHostView.bounds;
1947
2412
  [self applyHeaderOffset];
1948
2413
  }
1949
2414
 
1950
- - (void)offsetDiag:(NSString *)reason page:(NSInteger)index scroll:(UIScrollView *)scroll
2415
+ - (void)updatePinnedStickyHeader
1951
2416
  {
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
2417
+ BOOL attachedToList = _nativeSmoothHeaderScrollEnabled &&
2418
+ _sharedHeaderScrollView != nil && !_sharedHeadersLiftedForPagerTransition;
2419
+ if (!attachedToList && !_stickyHeaderIsPinned) return;
2420
+ UIScrollView *scrollView = _sharedHeaderScrollView;
2421
+ NSNumber *appliedTop = [_appliedTopInsets objectForKey:scrollView];
2422
+ CGFloat refreshInset = scrollView.refreshControl != nil && appliedTop != nil
2423
+ ? MAX(0, scrollView.contentInset.top - appliedTop.doubleValue) : 0;
2424
+ CGFloat pinnedTranslation = MAX(0, _currentLogicalOffset - _headerHeight - refreshInset);
2425
+ CGFloat stickyTop = [_sharedHeaderHostView convertPoint:
2426
+ CGPointMake(0, _headerHeight + pinnedTranslation) toView:self].y;
2427
+ BOOL shouldPin = _topScrollEdgeHard &&
2428
+ _stickyHeaderView != nil && attachedToList && !_transitioning &&
2429
+ !_externalOuterTransitioning &&
2430
+ !_isPagerDragging && _headerOffset >= _headerHeight - 0.5 &&
2431
+ stickyTop <= self.effectiveTopOverlayHeight + 0.5;
2432
+ if (_stickyHeaderIsPinned && shouldPin && _pinnedStickyHostView.superview != scrollView) {
2433
+ [self updateTopScrollEdgeForScrollView:nil];
2434
+ [scrollView addSubview:_pinnedStickyHostView];
2435
+ [self updateTopScrollEdgeForScrollView:_observedScrollView];
2436
+ }
2437
+ if (_stickyHeaderIsPinned == shouldPin) {
2438
+ if (shouldPin) {
2439
+ CGPoint origin = [scrollView convertPoint:CGPointMake(0, self.effectiveTopOverlayHeight)
2440
+ fromView:_containerView];
2441
+ _pinnedStickyHostView.frame = CGRectMake(origin.x, origin.y,
2442
+ CGRectGetWidth(self.bounds), _stickyHeaderHeight);
2443
+ }
2444
+ return;
2445
+ }
2446
+
2447
+ _stickyHeaderIsPinned = shouldPin;
2448
+ UIView *host = shouldPin ? _pinnedStickyHostView : _sharedHeaderHostView;
2449
+ _stickyHeaderView.transform = CGAffineTransformIdentity;
2450
+ _nativeTabBarView.transform = CGAffineTransformIdentity;
2451
+ _nativeSubHeaderView.transform = CGAffineTransformIdentity;
2452
+ [_stickyHeaderView removeFromSuperview];
2453
+ [_nativeTabBarView removeFromSuperview];
2454
+ [_nativeSubHeaderView removeFromSuperview];
2455
+ if (_stickyHeaderView != nil) [host addSubview:_stickyHeaderView];
2456
+ [host addSubview:_nativeTabBarView];
2457
+ [host addSubview:_nativeSubHeaderView];
2458
+ CGFloat childY = shouldPin ? 0 : _headerHeight;
2459
+ CGFloat width = CGRectGetWidth(self.bounds);
2460
+ CGFloat tabHeight = _nativeTabBarView.hidden ? 0 : MIN(_stickyHeaderHeight, _nativeTabBarHeight);
2461
+ _stickyHeaderView.frame = CGRectMake(0, childY, width, _stickyHeaderHeight);
2462
+ _nativeTabBarView.frame = CGRectMake(0, childY, width, tabHeight);
2463
+ _nativeSubHeaderView.frame = CGRectMake(0, childY + tabHeight, width,
2464
+ MIN(MAX(0, _stickyHeaderHeight - tabHeight), _nativeSubHeaderHeight));
2465
+ _pinnedStickyHostView.hidden = !shouldPin;
2466
+ if ([_pagerScrollView isKindOfClass:RNCCollapsiblePagerDirectScrollView.class]) {
2467
+ ((RNCCollapsiblePagerDirectScrollView *)_pagerScrollView).excludedHeaderView =
2468
+ shouldPin ? _pinnedStickyHostView : _sharedHeaderHostView;
2469
+ }
2470
+ if (shouldPin) {
2471
+ [scrollView addSubview:_pinnedStickyHostView];
2472
+ _pinnedStickyHostView.layer.zPosition = 1000;
2473
+ CGPoint origin = [scrollView convertPoint:CGPointMake(0, self.effectiveTopOverlayHeight)
2474
+ fromView:_containerView];
2475
+ _pinnedStickyHostView.frame = CGRectMake(origin.x, origin.y, width, _stickyHeaderHeight);
2476
+ } else {
2477
+ _pinnedStickyHostView.layer.zPosition = 0;
2478
+ [_containerView addSubview:_pinnedStickyHostView];
2479
+ }
2480
+ [_nativeTabBarView updateAncestorGesturePrecedence];
2481
+ [_nativeSubHeaderView updateAncestorGesturePrecedence];
2482
+ [self updateTopScrollEdgeForScrollView:_observedScrollView];
1967
2483
  }
1968
2484
 
1969
2485
  - (void)preparePageForHorizontalTransitionAtIndex:(NSInteger)pageIndex
1970
2486
  {
1971
- [self offsetDiag:@"prepare-check" page:pageIndex scroll:nil];
1972
2487
  if (!_nativeSmoothHeaderScrollEnabled ||
1973
2488
  pageIndex < 0 || pageIndex >= _pageControllers.count) return;
1974
2489
  UIScrollView *scrollView = [self verticalScrollViewForPageAtIndex:pageIndex];
@@ -1987,11 +2502,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1987
2502
  _transitionHeaderOffset = _headerOffset;
1988
2503
  _transitionPageOffsets = [_pageOffsets copy];
1989
2504
  _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
2505
  }
1996
2506
 
1997
2507
  - (void)updatePageStateTransitionProgress:(CGFloat)progress
@@ -2008,16 +2518,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2008
2518
  CGFloat firstQ = [_transitionPageOffsets[[self pageKeyForIndex:first]] doubleValue];
2009
2519
  CGFloat secondQ = [_transitionPageOffsets[[self pageKeyForIndex:second]] doubleValue];
2010
2520
  _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
2521
  _syntheticPageRestore = YES;
2022
2522
  NSInteger visibleFirst = MAX(0, MIN((NSInteger)floor(progress), (NSInteger)_pageControllers.count - 1));
2023
2523
  NSInteger visibleLast = MIN(visibleFirst + 1, (NSInteger)_pageControllers.count - 1);
@@ -2059,10 +2559,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2059
2559
  _transitionPageOffsets = nil;
2060
2560
  _transitionCompensatedPages = nil;
2061
2561
  [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
2562
  }
2067
2563
 
2068
2564
  #pragma mark - React mounting
@@ -2119,9 +2615,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2119
2615
  if (!_nativeSmoothHeaderScrollEnabled && _transitionPageOffsets == nil && !_syntheticPageRestore &&
2120
2616
  ![_pendingPageRestoreKeys containsObject:pageKey] &&
2121
2617
  (ownsObservedPage || _pageOffsets[pageKey] == nil)) {
2122
- [self offsetDiag:@"mount-release-save-before" page:index scroll:scrollView];
2123
2618
  _pageOffsets[pageKey] = @(scrollView.contentOffset.y + scrollView.contentInset.top);
2124
- [self offsetDiag:@"mount-release-save-after" page:index scroll:scrollView];
2125
2619
  }
2126
2620
  break;
2127
2621
  }
@@ -2137,6 +2631,27 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2137
2631
  - (void)mountingTransactionDidMount:(const MountingTransaction &)transaction
2138
2632
  withSurfaceTelemetry:(const SurfaceTelemetry &)surfaceTelemetry
2139
2633
  {
2634
+ if (_needsSlotRebuild && !_deferTopEdgeSlotRebuild &&
2635
+ !_transitioning && !_isPagerDragging) {
2636
+ [self rebuildSlots];
2637
+ }
2638
+ [self registerExternalOuterPager];
2639
+ if ([self resolveExternalTopOverlay]) {
2640
+ [self reapplyInsetsToObservedScrollView];
2641
+ [self setNeedsLayout];
2642
+ }
2643
+ if (_pendingTopOverlayAppearanceCommit) {
2644
+ _pendingTopOverlayAppearanceCommit = NO;
2645
+ BOOL localOverlayReady = _topOverlayView.superview == _topOverlayHostView &&
2646
+ _topOverlayView.window == self.window;
2647
+ BOOL externalOverlayReady = _externalTopOverlayTag > 0 &&
2648
+ _externalTopOverlayView.window == self.window;
2649
+ if (!_isBeingRecycled && _topScrollEdgeHard && self.window != nil &&
2650
+ (localOverlayReady || externalOverlayReady)) {
2651
+ [self updateTopScrollEdgeForScrollView:_observedScrollView];
2652
+ }
2653
+ }
2654
+ [self updateScrollEdgeContentMasks];
2140
2655
  [_mountingRemovedScrollViews removeAllObjects];
2141
2656
  if (!_nativeSmoothHeaderScrollEnabled || _isBeingRecycled || self.window == nil ||
2142
2657
  _currentIndex < 0 || _currentIndex >= _pageControllers.count) return;
@@ -2214,7 +2729,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2214
2729
  if (_isBeingRecycled) return;
2215
2730
  NSInteger safeIndex = MIN(MAX(index, 0), _logicalChildren.count);
2216
2731
  [_logicalChildren insertObject:childComponentView atIndex:safeIndex];
2217
- [self rebuildSlots];
2732
+ if (_topScrollEdgeHard || _topOverlayView != nil) {
2733
+ _needsSlotRebuild = YES;
2734
+ } else {
2735
+ [self rebuildSlots];
2736
+ }
2218
2737
  }
2219
2738
 
2220
2739
  - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
@@ -2225,16 +2744,32 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2225
2744
  if (existingIndex == NSNotFound) return;
2226
2745
  [childComponentView removeFromSuperview];
2227
2746
  [_logicalChildren removeObjectAtIndex:existingIndex];
2228
- [self rebuildSlots];
2747
+ if (_topScrollEdgeHard || _topOverlayView != nil) {
2748
+ _needsSlotRebuild = YES;
2749
+ } else {
2750
+ [self rebuildSlots];
2751
+ }
2229
2752
  }
2230
2753
 
2231
2754
  - (void)rebuildSlots
2232
2755
  {
2756
+ if (_deferTopEdgeSlotRebuild) {
2757
+ _needsSlotRebuild = YES;
2758
+ return;
2759
+ }
2233
2760
  if (_transitioning || _isPagerDragging) {
2234
2761
  _needsSlotRebuild = YES;
2235
2762
  return;
2236
2763
  }
2764
+ if (_topScrollEdgeHard &&
2765
+ _logicalChildren.count != _pageKeys.count + 2 &&
2766
+ _logicalChildren.count != _pageKeys.count + 3) {
2767
+ _needsSlotRebuild = YES;
2768
+ return;
2769
+ }
2237
2770
  _needsSlotRebuild = NO;
2771
+ _deferTopEdgeSlotRebuild = NO;
2772
+ [self restoreScrollEdgeContentMasks];
2238
2773
  [self detachScrollObserver];
2239
2774
  [self restoreSharedHeadersToContainer];
2240
2775
  // OneKey patch: Fabric reuses these views without resetting transforms
@@ -2246,11 +2781,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2246
2781
  _nativeSubHeaderView.transform = CGAffineTransformIdentity;
2247
2782
  [_headerView removeFromSuperview];
2248
2783
  [_stickyHeaderView removeFromSuperview];
2784
+ [self updateTopScrollEdgeForScrollView:nil];
2785
+ [_topOverlayView removeFromSuperview];
2249
2786
  for (UIViewController *controller in _pageControllers) {
2250
2787
  [controller.view removeFromSuperview];
2251
2788
  }
2252
2789
  _headerView = nil;
2253
2790
  _stickyHeaderView = nil;
2791
+ _topOverlayView = nil;
2792
+ [self updateTopScrollEdgeForScrollView:nil];
2254
2793
  [_pageControllers removeAllObjects];
2255
2794
 
2256
2795
  if (_logicalChildren.count > 0) {
@@ -2261,7 +2800,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2261
2800
  _stickyHeaderView = _logicalChildren[1];
2262
2801
  [_sharedHeaderHostView addSubview:_stickyHeaderView];
2263
2802
  }
2264
- for (NSInteger index = 2; index < _logicalChildren.count; index++) {
2803
+ BOOL hasOverlaySlot = _topScrollEdgeHard &&
2804
+ _logicalChildren.count == _pageKeys.count + 3;
2805
+ NSInteger pageSlotStart = hasOverlaySlot ? 3 : 2;
2806
+ if (hasOverlaySlot) {
2807
+ _topOverlayView = _logicalChildren[2];
2808
+ [_topOverlayHostView addSubview:_topOverlayView];
2809
+ }
2810
+ [self resolveExternalTopOverlay];
2811
+ for (NSInteger index = pageSlotStart; index < _logicalChildren.count; index++) {
2265
2812
  UIView *page = _logicalChildren[index];
2266
2813
  UIViewController *controller = [UIViewController new];
2267
2814
  [controller.view addSubview:page];
@@ -2300,6 +2847,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2300
2847
  if (!_nativeTabBarView.hidden) [_sharedHeaderHostView bringSubviewToFront:_nativeTabBarView];
2301
2848
  if (!_nativeSubHeaderView.hidden) [_sharedHeaderHostView bringSubviewToFront:_nativeSubHeaderView];
2302
2849
  [_containerView bringSubviewToFront:_sharedHeaderHostView];
2850
+ if (_stickyHeaderIsPinned) {
2851
+ [_pinnedStickyHostView.superview bringSubviewToFront:_pinnedStickyHostView];
2852
+ }
2853
+ if (self.hasFixedTopOverlay) [_containerView bringSubviewToFront:_topOverlayHostView];
2303
2854
  [self setNeedsLayout];
2304
2855
  dispatch_async(dispatch_get_main_queue(), ^{
2305
2856
  if (!self->_isBeingRecycled) {
@@ -2312,7 +2863,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2312
2863
  - (void)layoutSubviews
2313
2864
  {
2314
2865
  [super layoutSubviews];
2866
+ [self syncScrollEdgeAppearance];
2315
2867
  _containerView.frame = self.bounds;
2868
+ [self registerExternalOuterPager];
2869
+ BOOL externalHeightChanged = [self resolveExternalTopOverlay];
2316
2870
  if (_directNativePagerEnabled) {
2317
2871
  _pagerScrollView.frame = _containerView.bounds;
2318
2872
  } else {
@@ -2375,6 +2929,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2375
2929
  }
2376
2930
  }
2377
2931
  [self layoutSharedHeaders];
2932
+ if (externalHeightChanged) [self reapplyInsetsToObservedScrollView];
2933
+ [self updateTopScrollEdgeForScrollView:_observedScrollView];
2934
+ [self updateScrollEdgeContentMasks];
2378
2935
  [self updateHeaderSettledStateForceBusy:NO];
2379
2936
  dispatch_async(dispatch_get_main_queue(), ^{
2380
2937
  if (!self->_isBeingRecycled) {
@@ -2386,9 +2943,16 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2386
2943
 
2387
2944
  - (void)prepareForRecycle
2388
2945
  {
2946
+ [_externalOuterPager removeNestedOverlayObserver:self];
2947
+ _externalOuterPager = nil;
2948
+ [self cancelTopInsetReboundCheck];
2949
+ [self restoreScrollEdgeContentMasks];
2950
+ [self updateTopScrollEdgeForScrollView:nil];
2389
2951
  [self cancelHeaderSettledCheck];
2390
2952
  [self publishHeaderSettledAtTop:NO identityChanged:NO];
2391
2953
  _isBeingRecycled = YES;
2954
+ _pendingTopOverlayAppearanceCommit = NO;
2955
+ [self syncScrollEdgeAppearance];
2392
2956
  _generation++;
2393
2957
  [_mountingRemovedScrollViews removeAllObjects];
2394
2958
  _needsScrollObserverReattachAfterMount = NO;
@@ -2408,9 +2972,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2408
2972
  if (appliedTop != nil && scrollView.refreshControl != nil) {
2409
2973
  original.top += MAX(0, scrollView.contentInset.top - appliedTop.doubleValue);
2410
2974
  }
2411
- [self offsetDiag:@"release-original-inset-before" page:_currentIndex scroll:scrollView];
2412
2975
  scrollView.contentInset = original;
2413
- [self offsetDiag:@"release-original-inset-after" page:_currentIndex scroll:scrollView];
2414
2976
  }
2415
2977
  NSValue *indicatorValue = [_originalIndicatorInsets objectForKey:scrollView];
2416
2978
  if (indicatorValue != nil) {
@@ -2439,6 +3001,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2439
3001
  _nativeSubHeaderView.transform = CGAffineTransformIdentity;
2440
3002
  [_headerView removeFromSuperview];
2441
3003
  [_stickyHeaderView removeFromSuperview];
3004
+ [_topOverlayView removeFromSuperview];
2442
3005
  for (UIViewController *controller in _pageControllers) {
2443
3006
  [controller.view removeFromSuperview];
2444
3007
  }
@@ -2446,6 +3009,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2446
3009
  [_pageControllers removeAllObjects];
2447
3010
  _headerView = nil;
2448
3011
  _stickyHeaderView = nil;
3012
+ _topOverlayView = nil;
3013
+ _topOverlayHostView.hidden = YES;
3014
+ [self updateTopScrollEdgeForScrollView:nil];
2449
3015
  _pageViewController.dataSource = nil;
2450
3016
  _pageViewController.delegate = nil;
2451
3017
  _pagerScrollView.delegate = nil;
@@ -2478,6 +3044,13 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2478
3044
  _pendingInitialPage = 0;
2479
3045
  _headerHeight = 0;
2480
3046
  _stickyHeaderHeight = 0;
3047
+ _topOverlayHeight = 0;
3048
+ _externalTopOverlayTag = 0;
3049
+ _externalTopOverlayView = nil;
3050
+ _externalTopOverlayHeight = 0;
3051
+ _externalOuterPageActive = NO;
3052
+ _externalOuterTransitioning = NO;
3053
+ _topScrollEdgeHard = NO;
2481
3054
  _nativeTabBarHeight = 44;
2482
3055
  _nativeSubHeaderHeight = 74;
2483
3056
  _headerOffset = 0;
@@ -2501,6 +3074,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2501
3074
  _pendingGoToIndex = -1;
2502
3075
  _pendingGoToAnimated = YES;
2503
3076
  _needsSlotRebuild = NO;
3077
+ _deferTopEdgeSlotRebuild = NO;
2504
3078
  _hasAppliedInitialPage = NO;
2505
3079
  // OneKey patch: Fabric retains old props when recycling this view.
2506
3080
  _needsPropsReapply = YES;
@@ -2517,6 +3091,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2517
3091
  {
2518
3092
  const auto &oldViewProps = *std::static_pointer_cast<const RNCCollapsiblePagerViewProps>(_props);
2519
3093
  const auto &newViewProps = *std::static_pointer_cast<const RNCCollapsiblePagerViewProps>(props);
3094
+ _applyingTopOverlayInsetChange =
3095
+ oldViewProps.topScrollEdgeHard != newViewProps.topScrollEdgeHard ||
3096
+ oldViewProps.topOverlayHeight != newViewProps.topOverlayHeight;
2520
3097
 
2521
3098
  if (_needsPropsReapply || oldViewProps.initialPage != newViewProps.initialPage) {
2522
3099
  _pendingInitialPage = MAX(0, newViewProps.initialPage);
@@ -2566,6 +3143,43 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2566
3143
  _stickyHeaderHeight = MAX(0, newViewProps.stickyHeaderHeight);
2567
3144
  insetsChanged = YES;
2568
3145
  }
3146
+ if (_needsPropsReapply || oldViewProps.topScrollEdgeHard != newViewProps.topScrollEdgeHard) {
3147
+ _topScrollEdgeHard = newViewProps.topScrollEdgeHard;
3148
+ if (oldViewProps.topScrollEdgeHard || newViewProps.topScrollEdgeHard) {
3149
+ _deferTopEdgeSlotRebuild = YES;
3150
+ [self rebuildSlots];
3151
+ NSUInteger generation = _generation;
3152
+ dispatch_async(dispatch_get_main_queue(), ^{
3153
+ if (self->_generation != generation || self->_isBeingRecycled ||
3154
+ !self->_deferTopEdgeSlotRebuild) return;
3155
+ self->_deferTopEdgeSlotRebuild = NO;
3156
+ [self rebuildSlots];
3157
+ });
3158
+ }
3159
+ insetsChanged = YES;
3160
+ }
3161
+ if (_needsPropsReapply ||
3162
+ oldViewProps.topOverlayColorScheme != newViewProps.topOverlayColorScheme) {
3163
+ NSString *nextScheme = RCTNSStringFromString(newViewProps.topOverlayColorScheme);
3164
+ if (self.hasFixedTopOverlay && _topScrollEdgeHard &&
3165
+ _topOverlayColorScheme != nil &&
3166
+ ![_topOverlayColorScheme isEqualToString:nextScheme]) {
3167
+ _pendingTopOverlayAppearanceCommit = YES;
3168
+ [self updateTopScrollEdgeForScrollView:nil];
3169
+ }
3170
+ _topOverlayColorScheme = nextScheme;
3171
+ [self syncScrollEdgeAppearance];
3172
+ }
3173
+ if (_needsPropsReapply || oldViewProps.topOverlayHeight != newViewProps.topOverlayHeight) {
3174
+ _topOverlayHeight = MAX(0, newViewProps.topOverlayHeight);
3175
+ insetsChanged = YES;
3176
+ }
3177
+ if (_needsPropsReapply ||
3178
+ oldViewProps.externalTopOverlayTag != newViewProps.externalTopOverlayTag) {
3179
+ _externalTopOverlayTag = newViewProps.externalTopOverlayTag;
3180
+ [self registerExternalOuterPager];
3181
+ insetsChanged = [self resolveExternalTopOverlay] || insetsChanged;
3182
+ }
2569
3183
  if (_needsPropsReapply || oldViewProps.pageKeys != newViewProps.pageKeys) {
2570
3184
  NSString *json = RCTNSStringFromString(newViewProps.pageKeys);
2571
3185
  NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding];
@@ -2633,17 +3247,23 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2633
3247
 
2634
3248
  _needsPropsReapply = NO;
2635
3249
  [super updateProps:props oldProps:oldProps];
3250
+ [self updateTopScrollEdgeForScrollView:_observedScrollView];
2636
3251
  if (insetsChanged) {
2637
3252
  [self reapplyInsetsToObservedScrollView];
2638
3253
  [self setNeedsLayout];
2639
3254
  }
3255
+ _applyingTopOverlayInsetChange = NO;
2640
3256
  if (!_nativeTabBarView.hidden) {
2641
- [_sharedHeaderHostView bringSubviewToFront:_nativeTabBarView];
3257
+ [_nativeTabBarView.superview bringSubviewToFront:_nativeTabBarView];
2642
3258
  }
2643
3259
  if (!_nativeSubHeaderView.hidden) {
2644
- [_sharedHeaderHostView bringSubviewToFront:_nativeSubHeaderView];
3260
+ [_nativeSubHeaderView.superview bringSubviewToFront:_nativeSubHeaderView];
2645
3261
  }
2646
3262
  [_sharedHeaderHostView.superview bringSubviewToFront:_sharedHeaderHostView];
3263
+ if (_stickyHeaderIsPinned) {
3264
+ [_pinnedStickyHostView.superview bringSubviewToFront:_pinnedStickyHostView];
3265
+ }
3266
+ if (self.hasFixedTopOverlay) [_containerView bringSubviewToFront:_topOverlayHostView];
2647
3267
  }
2648
3268
 
2649
3269
  #pragma mark - Collapsible scroll coordination
@@ -2877,21 +3497,17 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2877
3497
  return;
2878
3498
  }
2879
3499
 
2880
- [self offsetDiag:@"observer-candidate" page:_currentIndex scroll:candidate];
2881
3500
  BOOL replacingCurrentScrollView = _observingContentOffset &&
2882
3501
  (!_nativeSmoothHeaderScrollEnabled || [_observedPageKey isEqualToString:self.currentPageKey]);
2883
3502
  [self detachScrollObserver];
2884
3503
  if (replacingCurrentScrollView) {
2885
3504
  // Legacy replacement discards body state; smooth replacement waits for layout.
2886
- [self offsetDiag:@"observer-replacement-cache-before" page:_currentIndex scroll:candidate];
2887
3505
  if (!_nativeSmoothHeaderScrollEnabled) _pageOffsets[self.currentPageKey] = @(_headerOffset);
2888
- [self offsetDiag:@"observer-replacement-cache-after" page:_currentIndex scroll:candidate];
2889
3506
  }
2890
3507
  _observedScrollView = candidate;
2891
3508
  _observedPageKey = [self.currentPageKey copy];
2892
3509
  _observedPageIndex = _currentIndex;
2893
3510
  _observedPageGeneration = _generation;
2894
- [self offsetDiag:@"observer-attached-before-restore" page:_currentIndex scroll:candidate];
2895
3511
  [self applyInsetsToScrollView:candidate pageIndex:_currentIndex restore:YES];
2896
3512
  [candidate addObserver:self
2897
3513
  forKeyPath:@"contentOffset"
@@ -2909,6 +3525,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2909
3525
  _observingContentOffset = YES;
2910
3526
  _needsScrollObserverReattachAfterMount = NO;
2911
3527
  _currentLogicalOffset = candidate.contentOffset.y + candidate.contentInset.top;
3528
+ [self updateTopScrollEdgeForScrollView:candidate];
2912
3529
  [self attachSharedHeadersToScrollView:candidate];
2913
3530
  [self updateSharedHeaderPressCancellationGesture];
2914
3531
  [self updateHeaderSettledStateForceBusy:NO];
@@ -2916,6 +3533,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2916
3533
 
2917
3534
  - (void)detachScrollObserver
2918
3535
  {
3536
+ [self cancelTopInsetReboundCheck];
3537
+ [self restorePinnedStickyHeader];
3538
+ [self updateTopScrollEdgeForScrollView:nil];
2919
3539
  [self cancelHeaderSettledCheck];
2920
3540
  _headerCommandOwner = nil;
2921
3541
  _headerSettledGeometry = nil;
@@ -2925,7 +3545,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2925
3545
  UIScrollView *scrollView = _observedScrollView;
2926
3546
  if (scrollView != nil) {
2927
3547
  [scrollView.panGestureRecognizer removeTarget:self action:@selector(observedListPanChanged:)];
2928
- [self offsetDiag:@"detach-save-before" page:_currentIndex scroll:scrollView];
2929
3548
  NSString *ownerKey = _nativeSmoothHeaderScrollEnabled ? _observedPageKey : self.currentPageKey;
2930
3549
  BOOL ownerKeyExists = ownerKey != nil && ([_pageKeys containsObject:ownerKey] ||
2931
3550
  (_observedPageIndex >= 0 && _observedPageIndex < _pageControllers.count &&
@@ -2933,7 +3552,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2933
3552
  if (ownerKey != nil && !_nativeSmoothHeaderScrollEnabled) {
2934
3553
  _pageOffsets[ownerKey] = @(scrollView.contentOffset.y + scrollView.contentInset.top);
2935
3554
  }
2936
- [self offsetDiag:@"detach-save-after" page:_currentIndex scroll:scrollView];
2937
3555
  if (_observingContentOffset) {
2938
3556
  [scrollView removeObserver:self
2939
3557
  forKeyPath:@"contentOffset"
@@ -2975,6 +3593,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2975
3593
  removeGestureRecognizer:_sharedHeaderOuterPagerGesture];
2976
3594
  }
2977
3595
  _sharedHeaderOuterPagerGesture.blockedPagerGestures = nil;
3596
+ _sharedHeaderOuterPagerGesture.trackedTouch = nil;
3597
+ _sharedHeaderOuterPagerGesture.protectsNestedBody = NO;
3598
+ _sharedHeaderOuterPagerGesture.cancelsTouchesInView = YES;
2978
3599
  _sharedHeaderPressCancellationGestureHost = nil;
2979
3600
  _reactTouchHandler = nil;
2980
3601
  }
@@ -3024,7 +3645,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3024
3645
  }
3025
3646
  }
3026
3647
  if (_sharedHeaderPressCancellationGesture.view == gestureHost &&
3027
- _sharedHeaderOuterPagerGesture.view == _sharedHeaderHostView &&
3648
+ _sharedHeaderOuterPagerGesture.view == _containerView &&
3028
3649
  _reactTouchHandler == reactTouchHandler) {
3029
3650
  _sharedHeaderOuterPagerGesture.blockedPagerGestures = outerPagerGestures;
3030
3651
  return;
@@ -3066,7 +3687,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3066
3687
  _reactTouchHandler = reactTouchHandler;
3067
3688
  _sharedHeaderPressCancellationGestureHost = gestureHost;
3068
3689
  [gestureHost addGestureRecognizer:_sharedHeaderPressCancellationGesture];
3069
- [_sharedHeaderHostView addGestureRecognizer:_sharedHeaderOuterPagerGesture];
3690
+ [_containerView addGestureRecognizer:_sharedHeaderOuterPagerGesture];
3070
3691
  RNCCollapsiblePagerLog([NSString stringWithFormat:
3071
3692
  @"press-cancel-bridge attached=1 outer-pagers=%lu",
3072
3693
  (unsigned long)outerPagerGestures.count]);
@@ -3138,6 +3759,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3138
3759
 
3139
3760
  - (void)observedListPanChanged:(UIPanGestureRecognizer *)recognizer
3140
3761
  {
3762
+ if (recognizer == _pendingTopInsetReboundScrollView.panGestureRecognizer &&
3763
+ (recognizer.state == UIGestureRecognizerStateBegan ||
3764
+ recognizer.state == UIGestureRecognizerStateChanged)) {
3765
+ [self cancelTopInsetReboundCheck];
3766
+ }
3141
3767
  if (recognizer == _observedScrollView.panGestureRecognizer) {
3142
3768
  [self updateHeaderSettledStateForceBusy:
3143
3769
  recognizer.state == UIGestureRecognizerStateBegan || recognizer.state == UIGestureRecognizerStateChanged];
@@ -3205,7 +3831,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3205
3831
  [_managedRefreshControls removeObjectForKey:scrollView];
3206
3832
  if (current != nil) [_managedRefreshControls setObject:current forKey:scrollView];
3207
3833
  }
3208
- [RNCRefreshControlOffsetState setOffset:_headerHeight + _stickyHeaderHeight
3834
+ [RNCRefreshControlOffsetState setOffset:self.effectiveTopOverlayHeight + _headerHeight + _stickyHeaderHeight
3209
3835
  forControl:current owner:self];
3210
3836
  }
3211
3837
 
@@ -3219,6 +3845,53 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3219
3845
  [_managedRefreshControls removeObjectForKey:scrollView];
3220
3846
  }
3221
3847
 
3848
+ - (void)cancelTopInsetReboundCheck
3849
+ {
3850
+ [_topInsetReboundTimer invalidate];
3851
+ _topInsetReboundTimer = nil;
3852
+ _pendingTopInsetReboundScrollView = nil;
3853
+ _pendingTopInsetReboundPageKey = nil;
3854
+ }
3855
+
3856
+ - (void)scheduleTopInsetReboundCheck
3857
+ {
3858
+ if (_pendingTopInsetReboundScrollView == nil ||
3859
+ _pendingTopInsetReboundScrollView != _observedScrollView) return;
3860
+ [_topInsetReboundTimer invalidate];
3861
+ __weak __typeof__(self) weakSelf = self;
3862
+ _topInsetReboundTimer = [NSTimer scheduledTimerWithTimeInterval:0.15 repeats:NO block:^(NSTimer *timer) {
3863
+ __strong __typeof__(weakSelf) self = weakSelf;
3864
+ if (self == nil || self->_topInsetReboundTimer != timer) return;
3865
+ self->_topInsetReboundTimer = nil;
3866
+ UIScrollView *scrollView = self->_pendingTopInsetReboundScrollView;
3867
+ if (scrollView == nil || scrollView != self->_observedScrollView ||
3868
+ self->_pendingTopInsetReboundGeneration != self->_generation ||
3869
+ ![self->_pendingTopInsetReboundPageKey isEqualToString:self.currentPageKey] ||
3870
+ self->_isBeingRecycled || self.window == nil ||
3871
+ self->_transitioning || self->_isPagerDragging ||
3872
+ self->_headerCommandOwner == scrollView || self->_pagerTopPageKey != nil) {
3873
+ [self cancelTopInsetReboundCheck];
3874
+ return;
3875
+ }
3876
+ UIGestureRecognizerState pan = scrollView.panGestureRecognizer.state;
3877
+ if (scrollView.refreshControl.isRefreshing || scrollView.isDragging ||
3878
+ scrollView.isDecelerating || scrollView.isTracking ||
3879
+ pan == UIGestureRecognizerStateBegan || pan == UIGestureRecognizerStateChanged ||
3880
+ self->_syntheticPageRestore || self->_transitionPageOffsets != nil) {
3881
+ [self scheduleTopInsetReboundCheck];
3882
+ return;
3883
+ }
3884
+ CGFloat minimumOffset = -scrollView.adjustedContentInset.top;
3885
+ [self cancelTopInsetReboundCheck];
3886
+ if (scrollView.contentOffset.y < minimumOffset - 0.5) {
3887
+ // UIKit can finish a refresh rebound at the inset that preceded an overlay resize.
3888
+ self->_correctingTopInsetRebound = YES;
3889
+ [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, minimumOffset) animated:NO];
3890
+ self->_correctingTopInsetRebound = NO;
3891
+ }
3892
+ }];
3893
+ }
3894
+
3222
3895
  // OneKey patch: release observers, headers and saved state while the scroll view
3223
3896
  // still represents the old list, before Fabric applies its next owner's props.
3224
3897
  - (void)restoreScrollViewState:(UIScrollView *)scrollView resetSavedOffset:(BOOL)resetSavedOffset
@@ -3227,9 +3900,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3227
3900
  [self releaseRefreshControlForScrollView:scrollView];
3228
3901
  if (scrollView == _observedScrollView) {
3229
3902
  [self detachScrollObserver];
3230
- [self offsetDiag:resetSavedOffset ? @"release-reset-before" : @"release-retain-before" page:_currentIndex scroll:scrollView];
3231
3903
  if (resetSavedOffset && !_nativeSmoothHeaderScrollEnabled) _pageOffsets[self.currentPageKey] = @(_headerOffset);
3232
- [self offsetDiag:@"release-reset-after" page:_currentIndex scroll:scrollView];
3233
3904
  }
3234
3905
  if (scrollView == _sharedHeaderScrollView) {
3235
3906
  [self restoreSharedHeadersToContainer];
@@ -3249,10 +3920,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3249
3920
  [_originalDirectionalLockEnabled objectForKey:scrollView].boolValue;
3250
3921
  scrollView.contentInsetAdjustmentBehavior = (UIScrollViewContentInsetAdjustmentBehavior)
3251
3922
  [_originalInsetAdjustmentBehavior objectForKey:scrollView].integerValue;
3252
- [self offsetDiag:@"release-original-offset-before" page:_currentIndex scroll:scrollView];
3253
3923
  [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, logicalOffset - original.top)
3254
3924
  animated:NO];
3255
- [self offsetDiag:@"release-original-offset-after" page:_currentIndex scroll:scrollView];
3256
3925
  [_originalInsets removeObjectForKey:scrollView];
3257
3926
  [_appliedTopInsets removeObjectForKey:scrollView];
3258
3927
  [_originalIndicatorInsets removeObjectForKey:scrollView];
@@ -3283,7 +3952,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3283
3952
  {
3284
3953
  // Smooth pages retain their complete offset while replacement identity and
3285
3954
  // layout settle. Legacy pages retain the historical header-only policy.
3286
- [self offsetDiag:restore ? @"restore-enter" : @"insets-enter" page:pageIndex scroll:scrollView];
3287
3955
  NSString *pageKey = [self pageKeyForIndex:pageIndex];
3288
3956
  RNCCollapsiblePagerReleasedScrollState *released = _releasedPageScrollStates[pageKey];
3289
3957
  BOOL deferredReleasedIdentity = NO;
@@ -3329,13 +3997,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3329
3997
  deferredReleasedIdentity = now - released.pendingFallbackDetectedAt <
3330
3998
  RNCCollapsiblePagerFallbackDebounceInterval;
3331
3999
  }
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
4000
  if (!deferredReleasedIdentity) {
3336
- [self offsetDiag:@"released-identity-cache-before" page:pageIndex scroll:scrollView];
3337
4001
  if (!sameList && !_nativeSmoothHeaderScrollEnabled) _pageOffsets[pageKey] = @(_headerOffset);
3338
- [self offsetDiag:@"released-identity-cache-after" page:pageIndex scroll:scrollView];
3339
4002
  [_releasedPageScrollStates removeObjectForKey:pageKey];
3340
4003
  }
3341
4004
  }
@@ -3360,22 +4023,24 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3360
4023
  // suppress its later vertical movement while the pager is still pending.
3361
4024
  scrollView.directionalLockEnabled = _nativeSmoothHeaderScrollEnabled
3362
4025
  ? NO : [_originalDirectionalLockEnabled objectForKey:scrollView].boolValue;
3363
- [self updateNativeScrollerViewport:scrollView stickyHeight:_stickyHeaderHeight];
4026
+ [self updateNativeScrollerViewport:scrollView stickyHeight:_stickyHeaderHeight + self.effectiveTopOverlayHeight];
3364
4027
  [self updateRefreshControlForScrollView:scrollView];
3365
4028
  CGFloat previousTopInset = scrollView.contentInset.top;
3366
4029
  CGFloat previousLogicalOffset = scrollView.contentOffset.y + previousTopInset;
4030
+ // Preserve UIKit's rebound after refresh while the content is beyond its top inset.
4031
+ BOOL wasReturningFromPull = scrollView.contentOffset.y < -scrollView.adjustedContentInset.top - 0.5;
3367
4032
  NSNumber *appliedTop = [_appliedTopInsets objectForKey:scrollView];
3368
4033
  // UIRefreshControl owns its temporary top inset, including while ending.
3369
4034
  CGFloat refreshInset = appliedTop != nil && scrollView.refreshControl != nil
3370
4035
  ? MAX(0, previousTopInset - appliedTop.doubleValue) : 0;
3371
4036
  UIEdgeInsets next = original;
3372
- next.top += _headerHeight + _stickyHeaderHeight;
4037
+ next.top += self.effectiveTopOverlayHeight + _headerHeight + _stickyHeaderHeight;
3373
4038
  CGFloat pagerTopInset = next.top;
3374
4039
  next.top += refreshInset;
3375
4040
  [_appliedTopInsets setObject:@(pagerTopInset) forKey:scrollView];
3376
4041
  // Short pages still need enough range to collapse the header; long pages
3377
4042
  // must end at the last row rather than leave a header-sized blank footer.
3378
- next.bottom += MAX(0, scrollView.bounds.size.height - _stickyHeaderHeight
4043
+ next.bottom += MAX(0, scrollView.bounds.size.height - _stickyHeaderHeight - self.effectiveTopOverlayHeight
3379
4044
  - original.top - original.bottom - scrollView.contentSize.height);
3380
4045
  if (UIEdgeInsetsEqualToEdgeInsets(scrollView.contentInset, next) && !restore) {
3381
4046
  // UICollectionView may report contentSize during a bounce without changing
@@ -3385,9 +4050,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3385
4050
  }
3386
4051
  BOOL previousSyntheticRestore = _syntheticPageRestore;
3387
4052
  if (_nativeSmoothHeaderScrollEnabled || [_alignedBodyKeys containsObject:pageKey]) _syntheticPageRestore = YES;
3388
- [self offsetDiag:@"inset-assignment-before" page:pageIndex scroll:scrollView];
3389
4053
  scrollView.contentInset = next;
3390
- [self offsetDiag:@"inset-assignment-after" page:pageIndex scroll:scrollView];
3391
4054
  UIEdgeInsets indicatorInsets = scrollView.verticalScrollIndicatorInsets;
3392
4055
  indicatorInsets.top = pagerTopInset;
3393
4056
  scrollView.verticalScrollIndicatorInsets = indicatorInsets;
@@ -3419,9 +4082,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3419
4082
  } else {
3420
4083
  restoredLogicalOffset = deferredReleasedIdentity || saved == nil ? _headerOffset : saved.doubleValue;
3421
4084
  }
3422
- #if DEBUG
3423
- [self offsetDiag:[NSString stringWithFormat:@"restore-candidate-saved=%.4f-headerAlignedLogical=%.4f", saved.doubleValue, restoredLogicalOffset] page:pageIndex scroll:scrollView];
3424
- #endif
3425
4085
  intendedLogicalOffset = restoredLogicalOffset;
3426
4086
  // Cancellation restores the visible source, while retaining its unreachable reading intent.
3427
4087
  if (_nativeSmoothHeaderScrollEnabled && _headerOffset >= _headerHeight &&
@@ -3434,9 +4094,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3434
4094
  }
3435
4095
  // An inactive list may shrink after its offset observer was detached.
3436
4096
  // 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
4097
  targetOffset = RNCLimitRestoredScrollOffset(
3441
4098
  targetOffset,
3442
4099
  scrollView.contentSize.height,
@@ -3445,13 +4102,17 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3445
4102
  next.bottom
3446
4103
  );
3447
4104
  }
3448
- #if DEBUG
3449
- [self offsetDiag:[NSString stringWithFormat:@"restore-final-target=%.4f-restore=%d", targetOffset, restore] page:pageIndex scroll:scrollView];
3450
- #endif
3451
4105
  // A bottom-inset change must not cancel UIKit's refresh or bounce settlement.
3452
- if (restore || previousTopInset != next.top) {
4106
+ if (restore || (previousTopInset != next.top && !wasReturningFromPull)) {
3453
4107
  [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, targetOffset) animated:NO];
3454
4108
  }
4109
+ if (_applyingTopOverlayInsetChange && !restore && wasReturningFromPull &&
4110
+ previousTopInset != next.top && scrollView == _observedScrollView) {
4111
+ _pendingTopInsetReboundScrollView = scrollView;
4112
+ _pendingTopInsetReboundPageKey = [pageKey copy];
4113
+ _pendingTopInsetReboundGeneration = _generation;
4114
+ [self scheduleTopInsetReboundCheck];
4115
+ }
3455
4116
  if (_nativeSmoothHeaderScrollEnabled && restore && targetOffset + next.top + 0.5 < intendedLogicalOffset) {
3456
4117
  [_pendingPageRestoreKeys addObject:pageKey];
3457
4118
  }
@@ -3461,7 +4122,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3461
4122
  if (!_nativeSmoothHeaderScrollEnabled) [self updateHeaderForScrollView:scrollView];
3462
4123
  else [self applyHeaderOffset];
3463
4124
  }
3464
- [self offsetDiag:@"restore-post" page:pageIndex scroll:scrollView];
3465
4125
  }
3466
4126
 
3467
4127
  - (void)updateNativeScrollerViewport:(UIScrollView *)scrollView stickyHeight:(CGFloat)height
@@ -3478,6 +4138,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3478
4138
  _observedPageKey == nil || ![_observedPageKey isEqualToString:self.currentPageKey] ||
3479
4139
  _observedPageIndex != _currentIndex || _observedPageGeneration != _generation ||
3480
4140
  _transitioning || _isPagerDragging || _transitionPageOffsets != nil || _syntheticPageRestore) return;
4141
+ [self cancelTopInsetReboundCheck];
3481
4142
  NSInteger phase = [notification.userInfo[@"phase"] integerValue];
3482
4143
  [self updateHeaderSettledStateForceBusy:NO];
3483
4144
  if (phase == 1) _headerCommandOwner = scrollView;
@@ -3510,7 +4171,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3510
4171
  _headerOffset = 0;
3511
4172
  [self applyHeaderOffset];
3512
4173
  }
3513
- [self offsetDiag:@"list-command-body-takeover" page:_currentIndex scroll:scrollView];
3514
4174
  }
3515
4175
 
3516
4176
  - (void)nativeScrollerWillReceiveScrollCommand:(RNCNativeScrollerComponentView *)scroller
@@ -3522,12 +4182,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3522
4182
  _observedPageIndex != _currentIndex || _currentIndex < 0 || _currentIndex >= _pageControllers.count ||
3523
4183
  _observedPageGeneration != _generation || _transitioning || _isPagerDragging ||
3524
4184
  _transitionPageOffsets != nil || _syntheticPageRestore) return;
4185
+ [self cancelTopInsetReboundCheck];
3525
4186
  if (!_dispatchingPagerTop) {
3526
4187
  [self cancelPagerTop];
3527
4188
  [_alignedBodyKeys removeObject:ownerKey];
3528
4189
  }
3529
4190
  if (!_nativeSmoothHeaderScrollEnabled) return;
3530
- [self offsetDiag:@"command-pending-cancel-before" page:_observedPageIndex scroll:scrollView];
3531
4191
  [_pendingPageRestoreKeys removeObject:ownerKey];
3532
4192
  _currentLogicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
3533
4193
  [_commandBodyKeys removeObject:ownerKey];
@@ -3536,7 +4196,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3536
4196
  [_displayBodyOverrides removeObjectForKey:ownerKey];
3537
4197
  _pageOffsets[ownerKey] = @(MAX(_currentLogicalOffset - _headerHeight, 0));
3538
4198
  [self updateHeaderForScrollView:scrollView];
3539
- [self offsetDiag:@"command-pending-cancel-after" page:_observedPageIndex scroll:scrollView];
3540
4199
  }
3541
4200
 
3542
4201
 
@@ -3598,6 +4257,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3598
4257
  return;
3599
4258
  }
3600
4259
  if (context == RNCCollapsiblePagerContentOffsetContext && object == _observedScrollView) {
4260
+ if (_pendingTopInsetReboundScrollView == object &&
4261
+ ([keyPath isEqualToString:@"contentOffset"] || [keyPath isEqualToString:@"contentInset"])) {
4262
+ if (((UIScrollView *)object).panGestureRecognizer.state == UIGestureRecognizerStateChanged) {
4263
+ [self cancelTopInsetReboundCheck];
4264
+ } else {
4265
+ [self scheduleTopInsetReboundCheck];
4266
+ }
4267
+ }
3601
4268
  if (![change[NSKeyValueChangeOldKey] isEqual:change[NSKeyValueChangeNewKey]]) {
3602
4269
  [self updateHeaderSettledStateForceBusy:YES];
3603
4270
  }
@@ -3662,18 +4329,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3662
4329
  }
3663
4330
  UIScrollView *scrollView = (UIScrollView *)object;
3664
4331
  _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
4332
+ if (_correctingTopInsetRebound) return;
3669
4333
  if ([_alignedBodyKeys containsObject:_observedPageKey] ||
3670
4334
  (_nativeSmoothHeaderScrollEnabled && scrollView.panGestureRecognizer.state != UIGestureRecognizerStateChanged &&
3671
4335
  !scrollView.isDecelerating && ![_commandBodyKeys containsObject:_observedPageKey])) return;
3672
4336
  _pageOffsets[_nativeSmoothHeaderScrollEnabled ? _observedPageKey : self.currentPageKey] =
3673
4337
  @(_nativeSmoothHeaderScrollEnabled ? MAX(_currentLogicalOffset - _headerHeight, 0) : _currentLogicalOffset);
3674
- #if DEBUG
3675
- if (offsetDiagChanged) [self offsetDiag:@"kvo-cache-after" page:_currentIndex scroll:scrollView];
3676
- #endif
3677
4338
  [self updateHeaderForScrollView:scrollView];
3678
4339
  return;
3679
4340
  }
@@ -3714,17 +4375,22 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3714
4375
  ? MAX(0, scrollView.contentInset.top - appliedTop.doubleValue) : 0;
3715
4376
  CGFloat pinnedTranslation = MAX(0, _currentLogicalOffset - _headerHeight - refreshInset);
3716
4377
  CGAffineTransform stickyTransform = CGAffineTransformMakeTranslation(0, pinnedTranslation);
3717
- _stickyHeaderView.transform = stickyTransform;
3718
- _nativeTabBarView.transform = stickyTransform;
3719
- _nativeSubHeaderView.transform = stickyTransform;
4378
+ [self updatePinnedStickyHeader];
4379
+ _stickyHeaderView.transform = _stickyHeaderIsPinned ? CGAffineTransformIdentity : stickyTransform;
4380
+ _nativeTabBarView.transform = _stickyHeaderIsPinned ? CGAffineTransformIdentity : stickyTransform;
4381
+ _nativeSubHeaderView.transform = _stickyHeaderIsPinned ? CGAffineTransformIdentity : stickyTransform;
4382
+ [self updateScrollEdgeContentMasks];
3720
4383
  return;
3721
4384
  }
4385
+ [self updatePinnedStickyHeader];
3722
4386
  CGAffineTransform collapseTransform = CGAffineTransformMakeTranslation(0, -_headerOffset);
3723
4387
  _sharedHeaderHostView.transform = collapseTransform;
3724
4388
  _headerView.transform = CGAffineTransformIdentity;
3725
4389
  _stickyHeaderView.transform = CGAffineTransformIdentity;
3726
4390
  _nativeTabBarView.transform = CGAffineTransformIdentity;
3727
4391
  _nativeSubHeaderView.transform = CGAffineTransformIdentity;
4392
+ [self updateTopScrollEdgeForScrollView:_observedScrollView];
4393
+ [self updateScrollEdgeContentMasks];
3728
4394
  }
3729
4395
 
3730
4396
  #pragma mark - Pager navigation
@@ -3736,7 +4402,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3736
4402
 
3737
4403
  - (void)goTo:(NSInteger)index animated:(BOOL)animated
3738
4404
  {
3739
- [self offsetDiag:@"go-to" page:index scroll:_observedScrollView];
4405
+ [self cancelTopInsetReboundCheck];
3740
4406
  if (_isBeingRecycled || index < 0 || index >= _pageControllers.count) return;
3741
4407
  _hasReceivedPageCommand = YES;
3742
4408
  _hasAppliedInitialPage = YES;
@@ -3867,6 +4533,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3867
4533
  if (!self->_transitioning && !self->_needsSlotRebuild) {
3868
4534
  [self attachScrollObserverForCurrentPage];
3869
4535
  }
4536
+ [self updateScrollEdgeContentMasks];
3870
4537
  });
3871
4538
  }
3872
4539
 
@@ -3918,7 +4585,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3918
4585
  - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
3919
4586
  {
3920
4587
  [self updateHeaderSettledStateForceBusy:YES];
3921
- [self offsetDiag:@"pager-drag-begin" page:_currentIndex scroll:_observedScrollView];
3922
4588
  if (_nativeSmoothHeaderScrollEnabled && scrollView == _pagerScrollView) {
3923
4589
  [_contentPressCancellationGesture cancelPressForPan:scrollView.panGestureRecognizer];
3924
4590
  }
@@ -4075,6 +4741,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4075
4741
  CGFloat offset = progress - position;
4076
4742
  position = MAX(0, MIN(position, (NSInteger)_pageControllers.count - 1));
4077
4743
  [self updatePageStateTransitionProgress:progress];
4744
+ [self updateScrollEdgeContentMasks];
4078
4745
  [_nativeTabBarView setProgress:progress];
4079
4746
  const auto emitter = [self eventEmitter];
4080
4747
  if (emitter) {
@@ -4093,6 +4760,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4093
4760
  tabProgress = _currentIndex + (_destinationIndex - _currentIndex) * transitionProgress;
4094
4761
  }
4095
4762
  [self updatePageStateTransitionProgress:tabProgress];
4763
+ [self updateScrollEdgeContentMasks];
4096
4764
  [_nativeTabBarView setProgress:tabProgress];
4097
4765
  const auto emitter = [self eventEmitter];
4098
4766
  if (emitter) {
@@ -4110,7 +4778,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4110
4778
 
4111
4779
  - (void)emitPageSelected:(NSInteger)position
4112
4780
  {
4113
- [self offsetDiag:@"selection-complete" page:position scroll:_observedScrollView];
4114
4781
  const auto emitter = [self eventEmitter];
4115
4782
  if (emitter) emitter->onPageSelected({.position = (double)position});
4116
4783
  }
@@ -4255,12 +4922,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4255
4922
  _currentLogicalOffset = logical;
4256
4923
  _headerOffset = _pagerTopLogicalTarget;
4257
4924
  [self applyHeaderOffset];
4258
- [self offsetDiag:[NSString stringWithFormat:@"pager-top-complete-token=%lu-target=%.4f", (unsigned long)_pagerTopToken, _pagerTopLogicalTarget] page:_currentIndex scroll:scrollView];
4259
4925
  [self cancelPagerTop];
4260
4926
  }
4261
4927
 
4262
4928
  - (void)scrollToTop:(NSInteger)target animated:(BOOL)animated
4263
4929
  {
4930
+ [self cancelTopInsetReboundCheck];
4264
4931
  UIScrollView *scrollView = _observedScrollView;
4265
4932
  NSString *key = _observedPageKey;
4266
4933
  if ((target != 0 && target != 1) || _isBeingRecycled || _transitioning || _isPagerDragging ||
@@ -4292,7 +4959,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4292
4959
  [self applyHeaderOffset];
4293
4960
  CGFloat physicalTarget = RNCLimitRestoredScrollOffset(logicalTarget - scrollView.contentInset.top,
4294
4961
  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
4962
  _dispatchingPagerTop = YES;
4297
4963
  [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, physicalTarget) animated:animated];
4298
4964
  _dispatchingPagerTop = NO;
@@ -4357,6 +5023,22 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4357
5023
  : velocity;
4358
5024
  BOOL hasMotion = MAX(fabs(intent.x), fabs(intent.y)) >= 1;
4359
5025
  BOOL horizontal = hasMotion && fabs(intent.x) > fabs(intent.y);
5026
+ if (_sharedHeaderOuterPagerGesture.protectsNestedBody) {
5027
+ NSInteger pageCount = _pageControllers.count;
5028
+ BOOL validPage = _currentIndex >= 0 && _currentIndex < pageCount;
5029
+ NSInteger firstPageIndex = self.isLtrLayout ? 0 : pageCount - 1;
5030
+ NSInteger lastPageIndex = self.isLtrLayout ? pageCount - 1 : 0;
5031
+ BOOL outward = validPage &&
5032
+ ((_currentIndex == firstPageIndex && intent.x > 0) ||
5033
+ (_currentIndex == lastPageIndex && intent.x < 0));
5034
+ UIScrollView *outer = RNCNestedOuterReactPagerScrollView(self);
5035
+ BOOL protectInnerPage = horizontal && validPage && pageCount > 1 &&
5036
+ _nestedScrollEnabled && _scrollEnabled && _pagerScrollView.scrollEnabled &&
5037
+ _pagerScrollView.panGestureRecognizer.enabled && outer != nil && outer.scrollEnabled &&
5038
+ [_sharedHeaderOuterPagerGesture.blockedPagerGestures
5039
+ containsObject:outer.panGestureRecognizer] && !outward;
5040
+ return protectInnerPage;
5041
+ }
4360
5042
  RNCCollapsiblePagerLog([NSString stringWithFormat:
4361
5043
  @"header-axis role=outer-pager-block dx=%.2f dy=%.2f velocity=(%.2f,%.2f) result=%d outer-pagers=%lu",
4362
5044
  intent.x,
@@ -4396,24 +5078,21 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4396
5078
  return vertical;
4397
5079
  }
4398
5080
  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];
5081
+ UIPanGestureRecognizer *pan = [gestureRecognizer isKindOfClass:UIPanGestureRecognizer.class]
5082
+ ? (UIPanGestureRecognizer *)gestureRecognizer : nil;
5083
+ CGPoint velocity = pan != nil ? [pan velocityInView:self] : CGPointZero;
4404
5084
  NSInteger pageCount = _pageControllers.count;
4405
- if (pageCount == 0 || _currentIndex < 0 || _currentIndex >= pageCount) return NO;
4406
-
4407
5085
  CGFloat absVelocityX = fabs(velocity.x);
4408
5086
  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;
5087
+ NSInteger firstPageIndex = self.isLtrLayout ? 0 : pageCount - 1;
5088
+ NSInteger lastPageIndex = self.isLtrLayout ? pageCount - 1 : 0;
5089
+ BOOL validPage = pageCount > 0 && _currentIndex >= 0 && _currentIndex < pageCount;
5090
+ BOOL horizontal = absVelocityX > absVelocityY && absVelocityX >= 1.0f;
5091
+ BOOL outward = validPage &&
5092
+ ((_currentIndex == firstPageIndex && velocity.x > 0) ||
5093
+ (_currentIndex == lastPageIndex && velocity.x < 0));
5094
+ BOOL shouldBegin = _nestedScrollEnabled && pan != nil && horizontal && outward;
5095
+ return shouldBegin;
4417
5096
  }
4418
5097
 
4419
5098
  - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
@@ -4423,13 +5102,29 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4423
5102
  _sharedHeaderPressCancellationGestureHost == nil) return NO;
4424
5103
  UIView *touchView = touch.view;
4425
5104
  BOOL beginsInSharedHeader = touchView == _sharedHeaderHostView ||
4426
- [touchView isDescendantOfView:_sharedHeaderHostView];
4427
- if (!beginsInSharedHeader) return NO;
5105
+ [touchView isDescendantOfView:_sharedHeaderHostView] ||
5106
+ touchView == _pinnedStickyHostView ||
5107
+ [touchView isDescendantOfView:_pinnedStickyHostView];
5108
+ if (_sharedHeaderOuterPagerGesture.trackedTouch != nil &&
5109
+ _sharedHeaderOuterPagerGesture.trackedTouch != touch) return NO;
5110
+ UIScrollView *outer = RNCNestedOuterReactPagerScrollView(self);
5111
+ BOOL beginsInNestedBody = !beginsInSharedHeader && _nestedScrollEnabled &&
5112
+ _scrollEnabled && _pagerScrollView.scrollEnabled &&
5113
+ _pagerScrollView.panGestureRecognizer.enabled && _pageControllers.count > 1 &&
5114
+ outer != nil && outer.scrollEnabled &&
5115
+ [_sharedHeaderOuterPagerGesture.blockedPagerGestures containsObject:outer.panGestureRecognizer] &&
5116
+ (touchView == _pagerScrollView || [touchView isDescendantOfView:_pagerScrollView]);
5117
+ if (!beginsInSharedHeader && !beginsInNestedBody) return NO;
5118
+
5119
+ _sharedHeaderOuterPagerGesture.trackedTouch = touch;
5120
+ _sharedHeaderOuterPagerGesture.protectsNestedBody = beginsInNestedBody;
5121
+ _sharedHeaderOuterPagerGesture.cancelsTouchesInView = !beginsInNestedBody;
4428
5122
 
4429
5123
  RNCCollapsiblePagerLog([NSString stringWithFormat:
4430
- @"outer-pager-touch target=%@ outer-pagers=%lu",
5124
+ @"outer-pager-touch target=%@ outer-pagers=%lu nested-body=%d",
4431
5125
  touchView == nil ? @"none" : NSStringFromClass(touchView.class),
4432
- (unsigned long)_sharedHeaderOuterPagerGesture.blockedPagerGestures.count]);
5126
+ (unsigned long)_sharedHeaderOuterPagerGesture.blockedPagerGestures.count,
5127
+ beginsInNestedBody]);
4433
5128
  return YES;
4434
5129
  }
4435
5130
  if (gestureRecognizer == _contentPressCancellationGesture) {
@@ -4438,14 +5133,17 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4438
5133
  return _nativeSmoothHeaderScrollEnabled && !_isBeingRecycled &&
4439
5134
  _contentPressCancellationGesture.view != nil &&
4440
5135
  [touchView isDescendantOfView:_pagerScrollView] &&
4441
- ![touchView isDescendantOfView:_sharedHeaderHostView];
5136
+ ![touchView isDescendantOfView:_sharedHeaderHostView] &&
5137
+ ![touchView isDescendantOfView:_pinnedStickyHostView];
4442
5138
  }
4443
5139
  if (gestureRecognizer == _sharedHeaderPressCancellationGesture) {
4444
5140
  if (!_nativeSmoothHeaderScrollEnabled || _isBeingRecycled ||
4445
5141
  _sharedHeaderPressCancellationGestureHost == nil) return NO;
4446
5142
  UIView *touchView = touch.view;
4447
5143
  BOOL beginsInSharedHeader = touchView == _sharedHeaderHostView ||
4448
- [touchView isDescendantOfView:_sharedHeaderHostView];
5144
+ [touchView isDescendantOfView:_sharedHeaderHostView] ||
5145
+ touchView == _pinnedStickyHostView ||
5146
+ [touchView isDescendantOfView:_pinnedStickyHostView];
4449
5147
  if (!beginsInSharedHeader) return NO;
4450
5148
 
4451
5149
  RNCCollapsiblePagerLog([NSString stringWithFormat:
@@ -4456,7 +5154,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
4456
5154
  if (gestureRecognizer == _verticalPagerGesture) {
4457
5155
  UIView *touchView = touch.view;
4458
5156
  return touchView != _sharedHeaderHostView &&
4459
- ![touchView isDescendantOfView:_sharedHeaderHostView];
5157
+ ![touchView isDescendantOfView:_sharedHeaderHostView] &&
5158
+ touchView != _pinnedStickyHostView &&
5159
+ ![touchView isDescendantOfView:_pinnedStickyHostView];
4460
5160
  }
4461
5161
  // A new touch can cancel UIKit's animation before its first frame. Queue a
4462
5162
  // non-animated settle for the next safe transition boundary instead of