@onekeyfe/react-native-pager-view 6.7.0-alpha.369 → 6.7.0-alpha.375
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.
- package/docs/SPEC.md +45 -0
- package/ios/RNCCollapsiblePagerViewComponentView.mm +231 -28
- package/ios/RNCNativeScrollerComponentView.h +2 -0
- package/ios/RNCNativeScrollerComponentView.mm +18 -0
- package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +186 -0
- package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +9 -9
- package/package.json +2 -2
package/docs/SPEC.md
CHANGED
|
@@ -817,6 +817,51 @@ fallback debounce, including replacement-state restoration. Other collection
|
|
|
817
817
|
fallbacks keep that safety delay. Caller insets and retained logical offsets
|
|
818
818
|
remain governed by the existing restoration policy.
|
|
819
819
|
|
|
820
|
+
When the active scroll owner is removed before its replacement becomes usable,
|
|
821
|
+
the pager MUST retain bounded recovery (at most 12 retries per recovery round,
|
|
822
|
+
spaced 100 ms apart). Clearing the completed mount's reattachment flag MUST NOT
|
|
823
|
+
cancel that recovery. A replacement that becomes usable during that round,
|
|
824
|
+
without another Fabric insertion, MUST receive the observer, shared headers,
|
|
825
|
+
insets and retained collapse state. Page selection changes and recycling
|
|
826
|
+
invalidate stale retry callbacks. Repeated mount attempts MUST NOT reset the
|
|
827
|
+
retry budget. Once all 12 retries have been scheduled, an Insert whose parent is
|
|
828
|
+
in the current page subtree MUST reopen recovery if that page retains a released
|
|
829
|
+
scroll owner, including when the final retry callback is still pending. Reopening
|
|
830
|
+
invalidates the old callback before clearing its scheduled state. The Insert
|
|
831
|
+
MUST be recorded during a horizontal drag or transition, while observer and shared
|
|
832
|
+
header attachment remain deferred until selection settles. Cancelling the drag
|
|
833
|
+
on the same page MUST retain the reopened recovery round. Empty transactions,
|
|
834
|
+
removals and inserts outside the current page MUST NOT reopen the budget. Owned
|
|
835
|
+
`NativeScroller` and `NativeList` hosts additionally emit push readiness
|
|
836
|
+
notifications when their scroll views attach and obtain positive bounds so late
|
|
837
|
+
replacements recover immediately even without another Fabric insertion or after
|
|
838
|
+
the retry budget expires; static pages with neither an unready scroll host nor a
|
|
839
|
+
released scroll owner do not schedule retry timers.
|
|
840
|
+
|
|
841
|
+
Status of transition-time and pending-final-retry recovery: implemented and
|
|
842
|
+
UIKit regression verified on iOS 26.5 (2026-10-27). Both new regressions fail
|
|
843
|
+
before the fix and pass afterward. Cancellation settles on the source page
|
|
844
|
+
before the replacement becomes usable; the pending-final-retry case makes the
|
|
845
|
+
replacement usable after the old timer's deadline. Observer and shared header
|
|
846
|
+
ownership stay detached during the drag, then recover with the retained collapse.
|
|
847
|
+
The full Pager suite executes 85 tests, with one iOS-27-only skip and no failures.
|
|
848
|
+
|
|
849
|
+
Status of exhausted-budget recovery: implemented and UIKit regression verified
|
|
850
|
+
on iOS 26.5 (2026-10-27). The regression exhausts the initial round, checks that
|
|
851
|
+
empty transactions, removals and offscreen inserts do not reopen it, then inserts
|
|
852
|
+
a current-page replacement whose scroller appears after did-mount. It fails
|
|
853
|
+
before the fix and passes afterward, including preservation of an active round
|
|
854
|
+
and restoration of the observer, shared headers and retained collapse. The full
|
|
855
|
+
Pager suite executes 83 tests, with one iOS-27-only skip and no failures.
|
|
856
|
+
|
|
857
|
+
Status of replacement recovery: implemented and runtime verified on iOS 26.5
|
|
858
|
+
(2026-10-23). The late-replacement UIKit regression fails before the fix and
|
|
859
|
+
passes afterward; the full Pager suite executes 82 tests, with one iOS-27-only
|
|
860
|
+
skip and no failures. In the consuming app based on x at b0789571d8 with SDK
|
|
861
|
+
6.7.0-9 and this local native change, two empty Favorites-to-Wallet-add-to-Market
|
|
862
|
+
cycles and a Trending/Favorites round trip keep the first row aligned with the
|
|
863
|
+
sticky header. The previously observed 212-point gap is absent.
|
|
864
|
+
|
|
820
865
|
This is a UI-thread, main-runtime native coordination change with no new props,
|
|
821
866
|
events, background-runtime work, persistence or retained strong view cache.
|
|
822
867
|
Android already prepares discovered scroll children before draw and is unchanged;
|
|
@@ -157,6 +157,20 @@ static NSString *const RNCCollapsiblePagerNativeScrollerIDPrefix =
|
|
|
157
157
|
|
|
158
158
|
static const CFTimeInterval RNCCollapsiblePagerFallbackDebounceInterval = 0.5;
|
|
159
159
|
|
|
160
|
+
typedef NS_ENUM(NSInteger, RNCScrollCandidateTier) {
|
|
161
|
+
RNCScrollCandidateTierNone = 0,
|
|
162
|
+
RNCScrollCandidateTierGenericFallback = 1,
|
|
163
|
+
RNCScrollCandidateTierVerifiedNativeList = 2,
|
|
164
|
+
RNCScrollCandidateTierFabricScrollView = 3,
|
|
165
|
+
RNCScrollCandidateTierExplicitScroller = 4,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
struct RNCScrollCandidateResolution {
|
|
169
|
+
UIScrollView *scrollView = nil;
|
|
170
|
+
RNCScrollCandidateTier tier = RNCScrollCandidateTierNone;
|
|
171
|
+
BOOL hasUnreadyScrollHost = NO;
|
|
172
|
+
};
|
|
173
|
+
|
|
160
174
|
// OneKey patch: native scroll views can survive a move or be reused for a new
|
|
161
175
|
// Fabric component. Keep weak identity until their page is attached again.
|
|
162
176
|
@interface RNCCollapsiblePagerReleasedScrollState : NSObject
|
|
@@ -1475,9 +1489,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1475
1489
|
- (void)updatePinnedStickyHeader;
|
|
1476
1490
|
- (UIScrollView *)currentVerticalScrollViewForParentPager;
|
|
1477
1491
|
- (UIScrollView *)verticalScrollViewForPageAtIndex:(NSInteger)pageIndex;
|
|
1492
|
+
- (RNCScrollCandidateResolution)resolveScrollCandidateForPageAtIndex:(NSInteger)pageIndex;
|
|
1493
|
+
- (RNCScrollCandidateResolution)resolveScrollCandidateInView:(UIView *)view;
|
|
1478
1494
|
- (UIScrollView *)findExplicitNativeScrollerInView:(UIView *)view;
|
|
1479
1495
|
- (UIScrollView *)findFabricScrollViewInView:(UIView *)view;
|
|
1480
1496
|
- (UIScrollView *)findVerticalScrollViewInView:(UIView *)view;
|
|
1497
|
+
- (void)scheduleCoalescedScrollReconcile;
|
|
1481
1498
|
- (void)scheduleScrollObserverRetryForCurrentPage;
|
|
1482
1499
|
- (void)preparePageForHorizontalTransitionAtIndex:(NSInteger)pageIndex;
|
|
1483
1500
|
- (void)updateSharedHeaderPressCancellationGesture;
|
|
@@ -1644,6 +1661,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1644
1661
|
RNCCollapsiblePagerOuterPagerPanGestureRecognizer *_sharedHeaderOuterPagerGesture;
|
|
1645
1662
|
RNCCollapsiblePagerVerticalPagerGuardGestureRecognizer *_verticalPagerGesture;
|
|
1646
1663
|
BOOL _observingContentOffset;
|
|
1664
|
+
BOOL _awaitingScrollOwnerForCurrentPage;
|
|
1665
|
+
BOOL _scrollReconcileScheduled;
|
|
1647
1666
|
BOOL _scrollResolveRetryScheduled;
|
|
1648
1667
|
CFTimeInterval _pendingFallbackScrollViewDetectedAt;
|
|
1649
1668
|
NSInteger _scrollResolveRetryPageIndex;
|
|
@@ -1675,6 +1694,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1675
1694
|
_displayBodyOverrides = [NSMutableDictionary new];
|
|
1676
1695
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(nativeListScrollCommandAccepted:)
|
|
1677
1696
|
name:@"OneKeyNativeListScrollCommandAccepted" object:nil];
|
|
1697
|
+
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(nativeScrollViewDidBecomeReadyNotification:)
|
|
1698
|
+
name:@"OneKeyNativeScrollViewDidBecomeReady" object:nil];
|
|
1678
1699
|
_pendingPageRestoreKeys = [NSMutableSet new];
|
|
1679
1700
|
_releasedPageScrollStates = [NSMutableDictionary new];
|
|
1680
1701
|
_mountingRemovedScrollViews = [NSHashTable weakObjectsHashTable];
|
|
@@ -2332,6 +2353,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2332
2353
|
[self updateTopScrollEdgeForScrollView:nil];
|
|
2333
2354
|
[self cancelHeaderSettledCheck];
|
|
2334
2355
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"OneKeyNativeListScrollCommandAccepted" object:nil];
|
|
2356
|
+
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"OneKeyNativeScrollViewDidBecomeReady" object:nil];
|
|
2335
2357
|
for (UIScrollView *scrollView in _originalInsets.keyEnumerator) {
|
|
2336
2358
|
[self updateNativeScrollerViewport:scrollView stickyHeight:0];
|
|
2337
2359
|
}
|
|
@@ -2874,6 +2896,23 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2874
2896
|
[self applyInsetsToScrollView:scrollView pageIndex:index restore:YES];
|
|
2875
2897
|
}
|
|
2876
2898
|
}
|
|
2899
|
+
UIView *page = _pageControllers[_currentIndex].view;
|
|
2900
|
+
if (_scrollResolveRetryAttempt >= 12 &&
|
|
2901
|
+
_releasedPageScrollStates[self.currentPageKey] != nil) {
|
|
2902
|
+
for (const auto &mutation : transaction.getMutations()) {
|
|
2903
|
+
if (mutation.type == ShadowViewMutation::Insert && mutation.parentTag > 0 &&
|
|
2904
|
+
[page viewWithTag:mutation.parentTag] != nil) {
|
|
2905
|
+
// A new current-page insertion starts a bounded recovery round after
|
|
2906
|
+
// the previous replacement search has scheduled its final retry.
|
|
2907
|
+
_scrollResolveRetryToken += 1;
|
|
2908
|
+
_scrollResolveRetryScheduled = NO;
|
|
2909
|
+
_scrollResolveRetryAttempt = 0;
|
|
2910
|
+
break;
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
// Record recovery during transitions; ownership still waits for settlement.
|
|
2877
2916
|
if (_transitioning || _isPagerDragging) return;
|
|
2878
2917
|
|
|
2879
2918
|
// OneKey patch: a release must recover even when the changed parent is above
|
|
@@ -2882,7 +2921,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2882
2921
|
[self attachScrollObserverForCurrentPage];
|
|
2883
2922
|
return;
|
|
2884
2923
|
}
|
|
2885
|
-
UIView *page = _pageControllers[_currentIndex].view;
|
|
2886
2924
|
BOOL headerIsAttached = [_sharedHeaderHostView isDescendantOfView:self];
|
|
2887
2925
|
BOOL currentListIsAttached = _observedScrollView != nil &&
|
|
2888
2926
|
[_observedScrollView isDescendantOfView:page] && headerIsAttached;
|
|
@@ -3056,12 +3094,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
3056
3094
|
}
|
|
3057
3095
|
if (self.hasFixedTopOverlay) [_containerView bringSubviewToFront:_topOverlayHostView];
|
|
3058
3096
|
[self setNeedsLayout];
|
|
3059
|
-
|
|
3060
|
-
if (!self->_isBeingRecycled) {
|
|
3061
|
-
[self prepareAdjacentPageInsets];
|
|
3062
|
-
[self attachScrollObserverForCurrentPage];
|
|
3063
|
-
}
|
|
3064
|
-
});
|
|
3097
|
+
[self scheduleCoalescedScrollReconcile];
|
|
3065
3098
|
}
|
|
3066
3099
|
|
|
3067
3100
|
- (void)layoutSubviews
|
|
@@ -3138,12 +3171,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
3138
3171
|
[self updateScrollEdgeContentMasks];
|
|
3139
3172
|
[self updateHeaderSettledStateForceBusy:NO];
|
|
3140
3173
|
[self logHomeVisualGeometry:@"layout" scrollView:_observedScrollView];
|
|
3141
|
-
|
|
3142
|
-
if (!self->_isBeingRecycled) {
|
|
3143
|
-
[self prepareAdjacentPageInsets];
|
|
3144
|
-
[self attachScrollObserverForCurrentPage];
|
|
3145
|
-
}
|
|
3146
|
-
});
|
|
3174
|
+
[self scheduleCoalescedScrollReconcile];
|
|
3147
3175
|
}
|
|
3148
3176
|
|
|
3149
3177
|
- (void)prepareForRecycle
|
|
@@ -3161,6 +3189,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
3161
3189
|
_generation++;
|
|
3162
3190
|
[_mountingRemovedScrollViews removeAllObjects];
|
|
3163
3191
|
_needsScrollObserverReattachAfterMount = NO;
|
|
3192
|
+
_awaitingScrollOwnerForCurrentPage = NO;
|
|
3193
|
+
_scrollReconcileScheduled = NO;
|
|
3164
3194
|
[_releasedPageScrollStates removeAllObjects];
|
|
3165
3195
|
[self detachSharedHeaderPressCancellationGesture];
|
|
3166
3196
|
[self detachScrollObserver];
|
|
@@ -3501,12 +3531,105 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
3501
3531
|
|
|
3502
3532
|
- (UIScrollView *)verticalScrollViewForPageAtIndex:(NSInteger)pageIndex
|
|
3503
3533
|
{
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3534
|
+
return [self resolveScrollCandidateForPageAtIndex:pageIndex].scrollView;
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
- (RNCScrollCandidateResolution)resolveScrollCandidateForPageAtIndex:(NSInteger)pageIndex
|
|
3538
|
+
{
|
|
3539
|
+
if (pageIndex < 0 || pageIndex >= _pageControllers.count) return {};
|
|
3540
|
+
return [self resolveScrollCandidateInView:_pageControllers[pageIndex].view];
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
// Single-pass tiered resolution replaces three separate subtree walks while
|
|
3544
|
+
// distinguishing genuinely static pages from pages with unready scroll hosts.
|
|
3545
|
+
- (RNCScrollCandidateResolution)resolveScrollCandidateInView:(UIView *)view
|
|
3546
|
+
{
|
|
3547
|
+
RNCScrollCandidateResolution resolution;
|
|
3548
|
+
if (view == nil || view == _sharedHeaderHostView) return resolution;
|
|
3549
|
+
if (view != self &&
|
|
3550
|
+
[view isKindOfClass:RNCCollapsiblePagerViewComponentView.class]) {
|
|
3551
|
+
UIScrollView *nested = [(RNCCollapsiblePagerViewComponentView *)view
|
|
3552
|
+
currentVerticalScrollViewForParentPager];
|
|
3553
|
+
if (nested != nil) {
|
|
3554
|
+
resolution.scrollView = nested;
|
|
3555
|
+
resolution.tier = RNCScrollCandidateTierExplicitScroller;
|
|
3556
|
+
} else {
|
|
3557
|
+
resolution.hasUnreadyScrollHost = YES;
|
|
3558
|
+
}
|
|
3559
|
+
return resolution;
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
Class nativeListClass = NSClassFromString(@"HybridNativeListComponent");
|
|
3563
|
+
if (nativeListClass != Nil && [view isKindOfClass:nativeListClass]) {
|
|
3564
|
+
resolution.hasUnreadyScrollHost = YES;
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
if ([view isKindOfClass:RNCNativeScrollerComponentView.class]) {
|
|
3568
|
+
UIScrollView *scrollView = ((RNCNativeScrollerComponentView *)view).scrollView;
|
|
3569
|
+
if (![_mountingRemovedScrollViews containsObject:scrollView]) {
|
|
3570
|
+
resolution.scrollView = scrollView;
|
|
3571
|
+
resolution.tier = RNCScrollCandidateTierExplicitScroller;
|
|
3572
|
+
return resolution;
|
|
3573
|
+
}
|
|
3574
|
+
} else if ([view isKindOfClass:RCTScrollViewComponentView.class]) {
|
|
3575
|
+
RCTScrollViewComponentView *componentView = (RCTScrollViewComponentView *)view;
|
|
3576
|
+
UIScrollView *scrollView = componentView.scrollView;
|
|
3577
|
+
if (![_mountingRemovedScrollViews containsObject:scrollView]) {
|
|
3578
|
+
if ([componentView.nativeId hasPrefix:RNCCollapsiblePagerNativeScrollerIDPrefix]) {
|
|
3579
|
+
resolution.scrollView = scrollView;
|
|
3580
|
+
resolution.tier = RNCScrollCandidateTierExplicitScroller;
|
|
3581
|
+
return resolution;
|
|
3582
|
+
}
|
|
3583
|
+
if (!scrollView.alwaysBounceHorizontal) {
|
|
3584
|
+
resolution.scrollView = scrollView;
|
|
3585
|
+
resolution.tier = RNCScrollCandidateTierFabricScrollView;
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
} else if ([view isKindOfClass:UICollectionView.class]) {
|
|
3589
|
+
UICollectionView *collectionView = (UICollectionView *)view;
|
|
3590
|
+
if (![_mountingRemovedScrollViews containsObject:collectionView]) {
|
|
3591
|
+
UICollectionViewLayout *layout = collectionView.collectionViewLayout;
|
|
3592
|
+
BOOL isVerticalLayout = ![layout isKindOfClass:UICollectionViewFlowLayout.class] ||
|
|
3593
|
+
((UICollectionViewFlowLayout *)layout).scrollDirection == UICollectionViewScrollDirectionVertical;
|
|
3594
|
+
if (isVerticalLayout) {
|
|
3595
|
+
BOOL hasNativeListHost = RNCNativeListComponentForScrollView(collectionView) != nil;
|
|
3596
|
+
BOOL isMountedNativeList = hasNativeListHost && RNCIsMountedNativeListScrollView(collectionView);
|
|
3597
|
+
BOOL hasUsableBounds = CGRectGetWidth(collectionView.bounds) > 0 &&
|
|
3598
|
+
CGRectGetHeight(collectionView.bounds) > 0;
|
|
3599
|
+
if (hasNativeListHost && !isMountedNativeList) {
|
|
3600
|
+
resolution.hasUnreadyScrollHost = YES;
|
|
3601
|
+
} else if (!hasUsableBounds) {
|
|
3602
|
+
resolution.hasUnreadyScrollHost = YES;
|
|
3603
|
+
} else {
|
|
3604
|
+
resolution.scrollView = collectionView;
|
|
3605
|
+
resolution.tier = isMountedNativeList
|
|
3606
|
+
? RNCScrollCandidateTierVerifiedNativeList
|
|
3607
|
+
: RNCScrollCandidateTierGenericFallback;
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
} else if ([view isKindOfClass:UIScrollView.class]) {
|
|
3612
|
+
UIScrollView *scrollView = (UIScrollView *)view;
|
|
3613
|
+
if (![_mountingRemovedScrollViews containsObject:scrollView] &&
|
|
3614
|
+
!scrollView.alwaysBounceHorizontal &&
|
|
3615
|
+
scrollView.contentSize.width <= scrollView.bounds.size.width + 1) {
|
|
3616
|
+
resolution.scrollView = scrollView;
|
|
3617
|
+
resolution.tier = RNCScrollCandidateTierGenericFallback;
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
for (UIView *subview in view.subviews) {
|
|
3622
|
+
RNCScrollCandidateResolution child = [self resolveScrollCandidateInView:subview];
|
|
3623
|
+
resolution.hasUnreadyScrollHost |= child.hasUnreadyScrollHost;
|
|
3624
|
+
if (child.tier > resolution.tier) {
|
|
3625
|
+
resolution.scrollView = child.scrollView;
|
|
3626
|
+
resolution.tier = child.tier;
|
|
3627
|
+
if (resolution.tier == RNCScrollCandidateTierExplicitScroller) {
|
|
3628
|
+
return resolution;
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
return resolution;
|
|
3510
3633
|
}
|
|
3511
3634
|
|
|
3512
3635
|
- (UIScrollView *)findExplicitNativeScrollerInView:(UIView *)view
|
|
@@ -3602,6 +3725,19 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
3602
3725
|
return nil;
|
|
3603
3726
|
}
|
|
3604
3727
|
|
|
3728
|
+
- (void)scheduleCoalescedScrollReconcile
|
|
3729
|
+
{
|
|
3730
|
+
if (_isBeingRecycled || _scrollReconcileScheduled) return;
|
|
3731
|
+
_scrollReconcileScheduled = YES;
|
|
3732
|
+
NSUInteger generation = _generation;
|
|
3733
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
3734
|
+
self->_scrollReconcileScheduled = NO;
|
|
3735
|
+
if (self->_isBeingRecycled || self->_generation != generation) return;
|
|
3736
|
+
[self prepareAdjacentPageInsets];
|
|
3737
|
+
[self attachScrollObserverForCurrentPage];
|
|
3738
|
+
});
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3605
3741
|
- (void)scheduleScrollObserverRetryForCurrentPage
|
|
3606
3742
|
{
|
|
3607
3743
|
if (_scrollResolveRetryScheduled || _scrollResolveRetryAttempt >= 12 ||
|
|
@@ -3654,29 +3790,35 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
3654
3790
|
_pendingFallbackScrollView = nil;
|
|
3655
3791
|
_pendingFallbackScrollViewDetectedAt = 0;
|
|
3656
3792
|
}
|
|
3657
|
-
|
|
3793
|
+
RNCScrollCandidateResolution resolution = [self resolveScrollCandidateForPageAtIndex:_currentIndex];
|
|
3794
|
+
UIScrollView *candidate = resolution.scrollView;
|
|
3658
3795
|
if (_nativeSmoothHeaderScrollEnabled && _currentIndex >= 0 && _currentIndex < _pageControllers.count) {
|
|
3659
3796
|
RNCGuardNestedHorizontalPans(_pageControllers[_currentIndex].view, _verticalPagerGesture);
|
|
3660
3797
|
}
|
|
3661
3798
|
if (candidate == nil) {
|
|
3662
3799
|
[self detachScrollObserver];
|
|
3663
3800
|
[self restoreSharedHeadersToContainer];
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3801
|
+
_needsScrollObserverReattachAfterMount = NO;
|
|
3802
|
+
_pendingFallbackScrollView = nil;
|
|
3803
|
+
_pendingFallbackScrollViewDetectedAt = 0;
|
|
3804
|
+
_awaitingScrollOwnerForCurrentPage = YES;
|
|
3805
|
+
// Only schedule bounded retry when the page has an unready scroll host in
|
|
3806
|
+
// its subtree or a released scroll owner awaiting replacement. Static pages
|
|
3807
|
+
// without a scroll host do not spin retry timers, and late replacements on
|
|
3808
|
+
// any page also attach deterministically via readiness notifications.
|
|
3809
|
+
BOOL hasReleasedOwnerAwaitingReplacement = _releasedPageScrollStates[self.currentPageKey] != nil;
|
|
3810
|
+
if (resolution.hasUnreadyScrollHost || hasReleasedOwnerAwaitingReplacement) {
|
|
3811
|
+
[self scheduleScrollObserverRetryForCurrentPage];
|
|
3812
|
+
} else {
|
|
3668
3813
|
_scrollResolveRetryToken += 1;
|
|
3669
3814
|
_scrollResolveRetryScheduled = NO;
|
|
3670
3815
|
_scrollResolveRetryAttempt = 0;
|
|
3671
|
-
_pendingFallbackScrollView = nil;
|
|
3672
|
-
_pendingFallbackScrollViewDetectedAt = 0;
|
|
3673
|
-
} else {
|
|
3674
|
-
[self scheduleScrollObserverRetryForCurrentPage];
|
|
3675
3816
|
}
|
|
3676
3817
|
return;
|
|
3677
3818
|
}
|
|
3678
3819
|
if ([candidate isKindOfClass:UICollectionView.class] &&
|
|
3679
3820
|
candidate != _observedScrollView && !RNCIsMountedNativeListScrollView(candidate)) {
|
|
3821
|
+
_awaitingScrollOwnerForCurrentPage = YES;
|
|
3680
3822
|
if (_pendingFallbackScrollView != candidate) {
|
|
3681
3823
|
_pendingFallbackScrollView = candidate;
|
|
3682
3824
|
_pendingFallbackScrollViewDetectedAt = CACurrentMediaTime();
|
|
@@ -3688,6 +3830,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
3688
3830
|
return;
|
|
3689
3831
|
}
|
|
3690
3832
|
}
|
|
3833
|
+
_awaitingScrollOwnerForCurrentPage = NO;
|
|
3691
3834
|
_scrollResolveRetryAttempt = 0;
|
|
3692
3835
|
_scrollResolveRetryPageIndex = _currentIndex;
|
|
3693
3836
|
_scrollResolveRetryToken += 1;
|
|
@@ -4511,6 +4654,66 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
4511
4654
|
}
|
|
4512
4655
|
|
|
4513
4656
|
|
|
4657
|
+
- (void)nativeScrollViewDidBecomeReadyNotification:(NSNotification *)notification
|
|
4658
|
+
{
|
|
4659
|
+
if ([notification.object isKindOfClass:UIScrollView.class]) {
|
|
4660
|
+
[self nativeScrollViewDidBecomeReady:(UIScrollView *)notification.object];
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
|
|
4664
|
+
- (void)nativeScrollViewDidBecomeReady:(UIScrollView *)scrollView
|
|
4665
|
+
{
|
|
4666
|
+
if (_isBeingRecycled || self.window == nil || scrollView == nil ||
|
|
4667
|
+
[_mountingRemovedScrollViews containsObject:scrollView]) return;
|
|
4668
|
+
for (UIView *ancestor = scrollView.superview;
|
|
4669
|
+
ancestor != nil && ancestor != self;
|
|
4670
|
+
ancestor = ancestor.superview) {
|
|
4671
|
+
if ([ancestor isKindOfClass:RNCCollapsiblePagerViewComponentView.class]) {
|
|
4672
|
+
RNCCollapsiblePagerViewComponentView *nestedPager =
|
|
4673
|
+
(RNCCollapsiblePagerViewComponentView *)ancestor;
|
|
4674
|
+
if ([nestedPager currentVerticalScrollViewForParentPager] != scrollView) return;
|
|
4675
|
+
break;
|
|
4676
|
+
}
|
|
4677
|
+
}
|
|
4678
|
+
NSInteger pageIndex = NSNotFound;
|
|
4679
|
+
for (NSInteger index = 0; index < _pageControllers.count; index++) {
|
|
4680
|
+
if ([scrollView isDescendantOfView:_pageControllers[index].view]) {
|
|
4681
|
+
pageIndex = index;
|
|
4682
|
+
break;
|
|
4683
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
if (pageIndex == NSNotFound) return;
|
|
4686
|
+
|
|
4687
|
+
NSString *pageKey = [self pageKeyForIndex:pageIndex];
|
|
4688
|
+
BOOL hasReleasedStateForPage = _releasedPageScrollStates[pageKey] != nil;
|
|
4689
|
+
BOOL currentPageNeedsOwner = pageIndex == _currentIndex &&
|
|
4690
|
+
(_awaitingScrollOwnerForCurrentPage || _needsScrollObserverReattachAfterMount ||
|
|
4691
|
+
hasReleasedStateForPage ||
|
|
4692
|
+
(_observedScrollView != nil &&
|
|
4693
|
+
[_observedScrollView isKindOfClass:UICollectionView.class] &&
|
|
4694
|
+
!RNCIsMountedNativeListScrollView(_observedScrollView) &&
|
|
4695
|
+
_observedScrollView != scrollView));
|
|
4696
|
+
|
|
4697
|
+
if (!hasReleasedStateForPage && !currentPageNeedsOwner &&
|
|
4698
|
+
!_needsScrollObserverReattachAfterMount) return;
|
|
4699
|
+
|
|
4700
|
+
if (_nativeSmoothHeaderScrollEnabled) {
|
|
4701
|
+
NSInteger first = MAX(0, _currentIndex - 1);
|
|
4702
|
+
NSInteger last = MIN((NSInteger)_pageControllers.count - 1, _currentIndex + 1);
|
|
4703
|
+
BOOL isOwnedScroller = [scrollView.superview isKindOfClass:RNCNativeScrollerComponentView.class] &&
|
|
4704
|
+
((RNCNativeScrollerComponentView *)scrollView.superview).scrollView == scrollView;
|
|
4705
|
+
if ((pageIndex >= first && pageIndex <= last || pageIndex == _destinationIndex) &&
|
|
4706
|
+
(RNCIsMountedNativeListScrollView(scrollView) || isOwnedScroller) &&
|
|
4707
|
+
[_originalInsets objectForKey:scrollView] == nil) {
|
|
4708
|
+
[self applyInsetsToScrollView:scrollView pageIndex:pageIndex restore:YES];
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4711
|
+
|
|
4712
|
+
if (currentPageNeedsOwner && !_transitioning && !_isPagerDragging) {
|
|
4713
|
+
[self attachScrollObserverForCurrentPage];
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4514
4717
|
- (void)nativeScroller:(RNCNativeScrollerComponentView *)scroller
|
|
4515
4718
|
callerInsetChangedFrom:(UIEdgeInsets)previous to:(UIEdgeInsets)next
|
|
4516
4719
|
{
|
|
@@ -13,6 +13,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
13
13
|
- (void)nativeScrollerWillReceiveScrollCommand:(RNCNativeScrollerComponentView *)scroller;
|
|
14
14
|
- (void)nativeScroller:(RNCNativeScrollerComponentView *)scroller
|
|
15
15
|
callerInsetChangedFrom:(UIEdgeInsets)previous to:(UIEdgeInsets)next;
|
|
16
|
+
@optional
|
|
17
|
+
- (void)nativeScrollViewDidBecomeReady:(UIScrollView *)scrollView;
|
|
16
18
|
@end
|
|
17
19
|
|
|
18
20
|
NS_ASSUME_NONNULL_END
|
|
@@ -298,10 +298,23 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
|
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
- (void)notifyCoordinatorScrollViewReadyIfNeeded
|
|
302
|
+
{
|
|
303
|
+
if (_recycled || self.window == nil ||
|
|
304
|
+
CGRectGetWidth(_scrollView.bounds) <= 0 || CGRectGetHeight(_scrollView.bounds) <= 0) return;
|
|
305
|
+
for (UIView *owner = self.superview; owner != nil; owner = owner.superview) {
|
|
306
|
+
if ([owner conformsToProtocol:@protocol(RNCNativeScrollerInsetCoordinator)] &&
|
|
307
|
+
[owner respondsToSelector:@selector(nativeScrollViewDidBecomeReady:)]) {
|
|
308
|
+
[(id<RNCNativeScrollerInsetCoordinator>)owner nativeScrollViewDidBecomeReady:_scrollView];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
301
313
|
- (void)didMoveToSuperview
|
|
302
314
|
{
|
|
303
315
|
[super didMoveToSuperview];
|
|
304
316
|
[self updateTopScrollEdge];
|
|
317
|
+
[self notifyCoordinatorScrollViewReadyIfNeeded];
|
|
305
318
|
}
|
|
306
319
|
|
|
307
320
|
- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
|
|
@@ -314,6 +327,10 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
|
|
|
314
327
|
if (!CGSizeEqualToSize(previous, _scrollView.bounds.size)) {
|
|
315
328
|
_needsContentClamp = YES;
|
|
316
329
|
[self scheduleContentClamp];
|
|
330
|
+
if ((previous.width <= 0 || previous.height <= 0) &&
|
|
331
|
+
_scrollView.bounds.size.width > 0 && _scrollView.bounds.size.height > 0) {
|
|
332
|
+
[self notifyCoordinatorScrollViewReadyIfNeeded];
|
|
333
|
+
}
|
|
317
334
|
}
|
|
318
335
|
}
|
|
319
336
|
|
|
@@ -574,6 +591,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
|
|
|
574
591
|
const auto &props = *std::static_pointer_cast<const RNCNativeScrollerProps>(_props);
|
|
575
592
|
if (props.refreshing) [_refreshControl beginRefreshing];
|
|
576
593
|
[self scheduleContentClamp];
|
|
594
|
+
[self notifyCoordinatorScrollViewReadyIfNeeded];
|
|
577
595
|
}
|
|
578
596
|
}
|
|
579
597
|
|
|
@@ -459,6 +459,138 @@ using namespace facebook::react;
|
|
|
459
459
|
XCTAssertTrue([[pager valueForKey:@"pendingPageRestoreKeys"] containsObject:@"page-1"]);
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
+
- (void)testReleasedOwnerRecoversReplacementWithoutAnotherFabricInsertion
|
|
463
|
+
{
|
|
464
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
|
|
465
|
+
// The fixture mounts page 0 directly; UIKit must not select it again.
|
|
466
|
+
[pager setValue:@YES forKey:@"hasAppliedInitialPage"];
|
|
467
|
+
RNCNativeScrollerComponentView *source = [self addScrollerToPager:pager atIndex:0];
|
|
468
|
+
source.tag = 91001;
|
|
469
|
+
[pager.window layoutIfNeeded];
|
|
470
|
+
[pager layoutIfNeeded];
|
|
471
|
+
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.15]];
|
|
472
|
+
[pager setValue:@120 forKey:@"headerOffset"];
|
|
473
|
+
[pager attachScrollObserverForCurrentPage];
|
|
474
|
+
XCTAssertEqual([pager valueForKey:@"observedScrollView"], source.scrollView);
|
|
475
|
+
|
|
476
|
+
// Finish the old owner's removal before the replacement becomes available.
|
|
477
|
+
ShadowView removedView;
|
|
478
|
+
removedView.tag = (Tag)source.tag;
|
|
479
|
+
MountingTransaction removal(1, 1, {ShadowViewMutation::RemoveMutation(1, removedView, 0)}, {});
|
|
480
|
+
SurfaceTelemetry telemetry;
|
|
481
|
+
[pager mountingTransactionWillMount:removal withSurfaceTelemetry:telemetry];
|
|
482
|
+
[source removeFromSuperview];
|
|
483
|
+
[self finishMount:pager];
|
|
484
|
+
XCTAssertNil([pager valueForKey:@"observedScrollView"]);
|
|
485
|
+
|
|
486
|
+
RNCNativeScrollerComponentView *replacement = [self addScrollerToPager:pager atIndex:0];
|
|
487
|
+
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]];
|
|
488
|
+
XCTAssertEqual([pager valueForKey:@"observedScrollView"], replacement.scrollView);
|
|
489
|
+
XCTAssertEqual([pager valueForKey:@"sharedHeaderScrollView"], replacement.scrollView);
|
|
490
|
+
XCTAssertEqualWithAccuracy(replacement.scrollView.contentOffset.y + replacement.scrollView.contentInset.top,
|
|
491
|
+
120, 0.01);
|
|
492
|
+
XCTAssertEqualWithAccuracy([[pager valueForKey:@"headerOffset"] doubleValue], 120, 0.01);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
- (void)testCurrentPageInsertionRestartsExhaustedReplacementRecovery
|
|
496
|
+
{
|
|
497
|
+
[self assertReplacementRecoveryWithCancelledDrag:NO finalRetryPending:NO];
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
- (void)testCancelledDragRetainsReplacementRecoveryFromInsertion
|
|
501
|
+
{
|
|
502
|
+
[self assertReplacementRecoveryWithCancelledDrag:YES finalRetryPending:NO];
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
- (void)testInsertionInvalidatesPendingFinalReplacementRetry
|
|
506
|
+
{
|
|
507
|
+
[self assertReplacementRecoveryWithCancelledDrag:NO finalRetryPending:YES];
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
- (void)assertReplacementRecoveryWithCancelledDrag:(BOOL)cancelDrag
|
|
511
|
+
finalRetryPending:(BOOL)finalRetryPending
|
|
512
|
+
{
|
|
513
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:2];
|
|
514
|
+
[pager setValue:@YES forKey:@"hasAppliedInitialPage"];
|
|
515
|
+
RNCNativeScrollerComponentView *source = [self addScrollerToPager:pager atIndex:0];
|
|
516
|
+
source.tag = 91001;
|
|
517
|
+
UIView *page = source.superview;
|
|
518
|
+
page.tag = 92001;
|
|
519
|
+
UIViewController *otherPage = [pager valueForKey:@"pageControllers"][1];
|
|
520
|
+
otherPage.view.tag = 92002;
|
|
521
|
+
[pager.window layoutIfNeeded];
|
|
522
|
+
[pager layoutIfNeeded];
|
|
523
|
+
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.15]];
|
|
524
|
+
[pager setValue:@120 forKey:@"headerOffset"];
|
|
525
|
+
[pager attachScrollObserverForCurrentPage];
|
|
526
|
+
XCTAssertEqual([pager valueForKey:@"observedScrollView"], source.scrollView);
|
|
527
|
+
|
|
528
|
+
ShadowView removedView;
|
|
529
|
+
removedView.tag = (Tag)source.tag;
|
|
530
|
+
MountingTransaction removal(1, 1, {ShadowViewMutation::RemoveMutation((Tag)page.tag, removedView, 0)}, {});
|
|
531
|
+
SurfaceTelemetry telemetry;
|
|
532
|
+
[pager mountingTransactionWillMount:removal withSurfaceTelemetry:telemetry];
|
|
533
|
+
[source removeFromSuperview];
|
|
534
|
+
[self finishMount:pager];
|
|
535
|
+
NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:3];
|
|
536
|
+
while (([[pager valueForKey:@"scrollResolveRetryAttempt"] integerValue] < 12 ||
|
|
537
|
+
(!finalRetryPending && [[pager valueForKey:@"scrollResolveRetryScheduled"] boolValue])) &&
|
|
538
|
+
deadline.timeIntervalSinceNow > 0) {
|
|
539
|
+
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];
|
|
540
|
+
}
|
|
541
|
+
XCTAssertEqualObjects([pager valueForKey:@"scrollResolveRetryAttempt"], @12);
|
|
542
|
+
XCTAssertEqualObjects([pager valueForKey:@"scrollResolveRetryScheduled"], @(finalRetryPending));
|
|
543
|
+
XCTAssertNotNil([pager valueForKey:@"releasedPageScrollStates"][@"page-0"]);
|
|
544
|
+
|
|
545
|
+
// Only a new insertion in the current page can reopen the exhausted budget.
|
|
546
|
+
[self finishMount:pager];
|
|
547
|
+
[pager mountingTransactionDidMount:removal withSurfaceTelemetry:telemetry];
|
|
548
|
+
UIView *replacementHost = [UIView new];
|
|
549
|
+
replacementHost.tag = 93001;
|
|
550
|
+
[otherPage.view addSubview:replacementHost];
|
|
551
|
+
ShadowView insertedView;
|
|
552
|
+
insertedView.tag = (Tag)replacementHost.tag;
|
|
553
|
+
MountingTransaction unrelated(1, 2, {ShadowViewMutation::InsertMutation((Tag)otherPage.view.tag, insertedView, 0)}, {});
|
|
554
|
+
[pager mountingTransactionDidMount:unrelated withSurfaceTelemetry:telemetry];
|
|
555
|
+
XCTAssertEqualObjects([pager valueForKey:@"scrollResolveRetryAttempt"], @12);
|
|
556
|
+
XCTAssertEqualObjects([pager valueForKey:@"scrollResolveRetryScheduled"], @(finalRetryPending));
|
|
557
|
+
|
|
558
|
+
if (cancelDrag) {
|
|
559
|
+
[pager beginPageStateTransition];
|
|
560
|
+
[pager setValue:@YES forKey:@"transitioning"];
|
|
561
|
+
[pager setValue:@YES forKey:@"isPagerDragging"];
|
|
562
|
+
}
|
|
563
|
+
[page addSubview:replacementHost];
|
|
564
|
+
MountingTransaction insertion(1, 3, {ShadowViewMutation::InsertMutation((Tag)page.tag, insertedView, 0)}, {});
|
|
565
|
+
[pager mountingTransactionDidMount:insertion withSurfaceTelemetry:telemetry];
|
|
566
|
+
XCTAssertNil([pager valueForKey:@"observedScrollView"]);
|
|
567
|
+
NSNumber *retryToken = [pager valueForKey:@"scrollResolveRetryToken"];
|
|
568
|
+
[pager mountingTransactionDidMount:insertion withSurfaceTelemetry:telemetry];
|
|
569
|
+
XCTAssertEqualObjects([pager valueForKey:@"scrollResolveRetryToken"], retryToken);
|
|
570
|
+
if (cancelDrag) {
|
|
571
|
+
XCTAssertNil([pager valueForKey:@"sharedHeaderScrollView"]);
|
|
572
|
+
XCTAssertFalse([[pager valueForKey:@"scrollResolveRetryScheduled"] boolValue]);
|
|
573
|
+
// Settle on the source page while the replacement is still unavailable.
|
|
574
|
+
[pager attachScrollObserverForCurrentPageAfterTransition:YES];
|
|
575
|
+
[pager setValue:@NO forKey:@"isPagerDragging"];
|
|
576
|
+
[pager setValue:@NO forKey:@"transitioning"];
|
|
577
|
+
[pager attachScrollObserverForCurrentPage];
|
|
578
|
+
XCTAssertEqualObjects([pager valueForKey:@"currentIndex"], @0);
|
|
579
|
+
}
|
|
580
|
+
__block RNCNativeScrollerComponentView *replacement;
|
|
581
|
+
int64_t replacementDelay = finalRetryPending ? 150 : 50;
|
|
582
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, replacementDelay * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
|
|
583
|
+
replacement = [self addScrollerToPager:pager atIndex:0];
|
|
584
|
+
[replacementHost addSubview:replacement];
|
|
585
|
+
});
|
|
586
|
+
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]];
|
|
587
|
+
XCTAssertEqual([pager valueForKey:@"observedScrollView"], replacement.scrollView);
|
|
588
|
+
XCTAssertEqual([pager valueForKey:@"sharedHeaderScrollView"], replacement.scrollView);
|
|
589
|
+
XCTAssertEqualWithAccuracy(replacement.scrollView.contentOffset.y + replacement.scrollView.contentInset.top,
|
|
590
|
+
120, 0.01);
|
|
591
|
+
XCTAssertEqualWithAccuracy([[pager valueForKey:@"headerOffset"] doubleValue], 120, 0.01);
|
|
592
|
+
}
|
|
593
|
+
|
|
462
594
|
- (void)testRenamedPageDoesNotResurrectPrunedObserverCache
|
|
463
595
|
{
|
|
464
596
|
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
|
|
@@ -603,4 +735,58 @@ using namespace facebook::react;
|
|
|
603
735
|
XCTAssertEqual(removed.count, 0u);
|
|
604
736
|
}
|
|
605
737
|
|
|
738
|
+
- (void)testEmptyPageWithoutReleasedOwnerOrScrollHostDoesNotScheduleRetry
|
|
739
|
+
{
|
|
740
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
|
|
741
|
+
[pager attachScrollObserverForCurrentPage];
|
|
742
|
+
XCTAssertNil([pager valueForKey:@"observedScrollView"]);
|
|
743
|
+
XCTAssertFalse([[pager valueForKey:@"scrollResolveRetryScheduled"] boolValue]);
|
|
744
|
+
XCTAssertEqual([[pager valueForKey:@"scrollResolveRetryAttempt"] integerValue], 0);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
- (void)testLateReplacementRecoversImmediatelyOnReadinessEvenAfterRetryBudgetExpires
|
|
748
|
+
{
|
|
749
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
|
|
750
|
+
RNCNativeScrollerComponentView *previous = [self addScrollerToPager:pager atIndex:0];
|
|
751
|
+
previous.tag = 91003;
|
|
752
|
+
[pager attachScrollObserverForCurrentPage];
|
|
753
|
+
UIView *sharedHeader = [pager valueForKey:@"sharedHeaderHostView"];
|
|
754
|
+
XCTAssertEqual([pager valueForKey:@"observedScrollView"], previous.scrollView);
|
|
755
|
+
|
|
756
|
+
ShadowView removedView;
|
|
757
|
+
removedView.tag = (Tag)previous.tag;
|
|
758
|
+
MountingTransaction removal(1, 1, {ShadowViewMutation::RemoveMutation(1, removedView, 0)}, {});
|
|
759
|
+
SurfaceTelemetry telemetry;
|
|
760
|
+
[pager mountingTransactionWillMount:removal withSurfaceTelemetry:telemetry];
|
|
761
|
+
[previous removeFromSuperview];
|
|
762
|
+
[self finishMount:pager];
|
|
763
|
+
XCTAssertNil([pager valueForKey:@"observedScrollView"]);
|
|
764
|
+
|
|
765
|
+
// Simulate retry budget exhaustion before the replacement becomes ready.
|
|
766
|
+
[pager setValue:@12 forKey:@"scrollResolveRetryAttempt"];
|
|
767
|
+
[pager setValue:@NO forKey:@"scrollResolveRetryScheduled"];
|
|
768
|
+
NSUInteger oldToken = [[pager valueForKey:@"scrollResolveRetryToken"] unsignedIntegerValue];
|
|
769
|
+
[pager setValue:@(oldToken + 1) forKey:@"scrollResolveRetryToken"];
|
|
770
|
+
|
|
771
|
+
UIViewController *page = [pager valueForKey:@"pageControllers"][0];
|
|
772
|
+
UICollectionView *lateList = [[UICollectionView alloc]
|
|
773
|
+
initWithFrame:CGRectMake(0, 0, 320, 600)
|
|
774
|
+
collectionViewLayout:[UICollectionViewFlowLayout new]];
|
|
775
|
+
[page.view addSubview:lateList];
|
|
776
|
+
|
|
777
|
+
// Readiness notification triggers immediate reconcile even after retry budget expired.
|
|
778
|
+
[[NSNotificationCenter defaultCenter]
|
|
779
|
+
postNotificationName:@"OneKeyNativeScrollViewDidBecomeReady"
|
|
780
|
+
object:lateList];
|
|
781
|
+
XCTAssertEqualObjects([pager valueForKey:@"pendingFallbackScrollView"], lateList);
|
|
782
|
+
|
|
783
|
+
// Explicit scroller readiness attaches synchronously with zero debounce delay.
|
|
784
|
+
[lateList removeFromSuperview];
|
|
785
|
+
RNCNativeScrollerComponentView *lateScroller = [self addScrollerToPager:pager atIndex:0];
|
|
786
|
+
XCTAssertEqual([pager valueForKey:@"observedScrollView"], lateScroller.scrollView);
|
|
787
|
+
XCTAssertEqual(sharedHeader.superview, lateScroller.scrollView);
|
|
788
|
+
XCTAssertEqualWithAccuracy(lateScroller.scrollView.contentInset.top, 164, 0.01);
|
|
789
|
+
XCTAssertNil([pager valueForKey:@"releasedPageScrollStates"][@"page-0"]);
|
|
790
|
+
}
|
|
791
|
+
|
|
606
792
|
@end
|
|
@@ -668,6 +668,7 @@ typedef struct __IOHIDEvent *RNCTestHIDEventRef;
|
|
|
668
668
|
@property (nonatomic, strong) RNCSynthesizedTouchDriver *driver;
|
|
669
669
|
@property (nonatomic, strong) UIView *firstPage;
|
|
670
670
|
@property (nonatomic) NSUInteger longPressBegins;
|
|
671
|
+
@property (nonatomic, strong) XCTestExpectation *longPressExpectation;
|
|
671
672
|
@property (nonatomic) NSUInteger pagerPanBegins;
|
|
672
673
|
@end
|
|
673
674
|
|
|
@@ -1001,7 +1002,10 @@ typedef struct __IOHIDEvent *RNCTestHIDEventRef;
|
|
|
1001
1002
|
|
|
1002
1003
|
- (void)nestedLongPress:(UILongPressGestureRecognizer *)recognizer
|
|
1003
1004
|
{
|
|
1004
|
-
if (recognizer.state == UIGestureRecognizerStateBegan)
|
|
1005
|
+
if (recognizer.state == UIGestureRecognizerStateBegan) {
|
|
1006
|
+
self.longPressBegins += 1;
|
|
1007
|
+
[self.longPressExpectation fulfill];
|
|
1008
|
+
}
|
|
1005
1009
|
}
|
|
1006
1010
|
|
|
1007
1011
|
- (CGPoint)pressPointInNestedScrollView:(UIScrollView *)scroll
|
|
@@ -1022,14 +1026,12 @@ typedef struct __IOHIDEvent *RNCTestHIDEventRef;
|
|
|
1022
1026
|
[pager addGestureRecognizer:longPress];
|
|
1023
1027
|
// Exercise production wiring after a root recognizer is attached.
|
|
1024
1028
|
((void (*)(id, SEL))objc_msgSend)(self.pager, NSSelectorFromString(@"connectVerticalPagerGuard"));
|
|
1029
|
+
self.longPressExpectation = [self expectationWithDescription:@"root long press recognized before finger-up"];
|
|
1025
1030
|
UITouch *touch = [self.driver beginAt:[self pressPoint]];
|
|
1026
1031
|
UIGestureRecognizer *guard = [self.pager valueForKey:@"verticalPagerGesture"];
|
|
1027
1032
|
XCTAssertEqual(guard.state, UIGestureRecognizerStatePossible);
|
|
1028
1033
|
XCTAssertEqual(pager.panGestureRecognizer.minimumNumberOfTouches, 2);
|
|
1029
|
-
|
|
1030
|
-
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)),
|
|
1031
|
-
dispatch_get_main_queue(), ^{ [hold fulfill]; });
|
|
1032
|
-
[self waitForExpectations:@[hold] timeout:2];
|
|
1034
|
+
[self waitForExpectations:@[self.longPressExpectation] timeout:2];
|
|
1033
1035
|
NSLog(@"[PagerPressDelivery] rootLongPress begins=%lu state=%ld touchPhase=%ld guardState=%ld pagerPanMinTouches=%lu",
|
|
1034
1036
|
(unsigned long)self.longPressBegins, (long)longPress.state, (long)touch.phase,
|
|
1035
1037
|
(long)guard.state, (unsigned long)pager.panGestureRecognizer.minimumNumberOfTouches);
|
|
@@ -1045,14 +1047,12 @@ typedef struct __IOHIDEvent *RNCTestHIDEventRef;
|
|
|
1045
1047
|
- (void)testNestedStationaryNativeLongPressRecognizesBeforeFingerUp
|
|
1046
1048
|
{
|
|
1047
1049
|
UIScrollView *scroll = [self mountNestedHorizontalScrollView];
|
|
1050
|
+
self.longPressExpectation = [self expectationWithDescription:@"nested long press recognized before finger-up"];
|
|
1048
1051
|
UITouch *touch = [self.driver beginAt:[self pressPointInNestedScrollView:scroll]];
|
|
1049
1052
|
UIGestureRecognizer *guard = [self.pager valueForKey:@"verticalPagerGesture"];
|
|
1050
1053
|
XCTAssertEqual(guard.state, UIGestureRecognizerStatePossible);
|
|
1051
1054
|
XCTAssertEqual(scroll.panGestureRecognizer.minimumNumberOfTouches, 2);
|
|
1052
|
-
|
|
1053
|
-
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)),
|
|
1054
|
-
dispatch_get_main_queue(), ^{ [hold fulfill]; });
|
|
1055
|
-
[self waitForExpectations:@[hold] timeout:2];
|
|
1055
|
+
[self waitForExpectations:@[self.longPressExpectation] timeout:2];
|
|
1056
1056
|
// The real UIKit target action must arrive while the finger is still down.
|
|
1057
1057
|
XCTAssertEqual(self.longPressBegins, 1);
|
|
1058
1058
|
XCTAssertEqual(touch.phase, UITouchPhaseBegan);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/react-native-pager-view",
|
|
3
|
-
"version": "6.7.0-alpha.
|
|
3
|
+
"version": "6.7.0-alpha.375",
|
|
4
4
|
"description": "React Native wrapper for Android and iOS ViewPager",
|
|
5
5
|
"source": "./src/index.tsx",
|
|
6
6
|
"main": "./lib/module/index.js",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"typescript": "^5.9.2"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
|
-
"@onekeyfe/react-native-native-logger": "6.7.0-alpha.
|
|
76
|
+
"@onekeyfe/react-native-native-logger": "6.7.0-alpha.375",
|
|
77
77
|
"react": "*",
|
|
78
78
|
"react-native": "*"
|
|
79
79
|
},
|