@onekeyfe/react-native-pager-view 6.7.0-2 → 6.7.0-20
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/README.md +14 -0
- package/android/src/androidTest/AndroidManifest.xml +7 -0
- package/android/src/androidTest/java/com/reactnativepagerview/CollapsiblePagerTabReservationTest.kt +306 -0
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +90 -21
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerNativeHeaders.kt +72 -7
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +27 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerScrollAwayInsets.kt +19 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +50 -6
- package/android/src/test/java/com/reactnativepagerview/NativeScrollerScrollAwayInsetsTest.kt +40 -0
- package/docs/SPEC.md +253 -10
- package/ios/RNCCollapsiblePagerViewComponentView.mm +614 -42
- package/ios/RNCNativeScrollerComponentView.h +2 -0
- package/ios/RNCNativeScrollerComponentView.mm +42 -0
- package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +393 -0
- package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +338 -0
- package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +74 -9
- package/ios/tests/RNCCollapsiblePagerRefreshControlTests.mm +50 -0
- package/lib/module/CollapsiblePagerView.js +32 -7
- package/lib/module/CollapsiblePagerView.web.js +439 -39
- package/lib/module/CollapsiblePagerViewNativeComponent.ts +13 -0
- package/lib/module/NativeScroller.web.js +16 -3
- package/lib/module/WebCollapsiblePagerPage.js +182 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.native.js +9 -0
- package/lib/module/index.web.js +2 -0
- package/lib/typescript/src/CollapsiblePagerView.d.ts +10 -3
- package/lib/typescript/src/CollapsiblePagerView.web.d.ts +66 -3
- package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +5 -0
- package/lib/typescript/src/WebCollapsiblePagerPage.d.ts +42 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.native.d.ts +13 -0
- package/lib/typescript/src/index.web.d.ts +4 -0
- package/package.json +6 -4
- package/react-native-pager-view.podspec +1 -0
- package/src/CollapsiblePagerView.tsx +99 -8
- package/src/CollapsiblePagerView.web.tsx +657 -51
- package/src/CollapsiblePagerViewNativeComponent.ts +13 -0
- package/src/NativeScroller.web.tsx +24 -2
- package/src/WebCollapsiblePagerPage.tsx +258 -0
- package/src/index.native.tsx +30 -0
- package/src/index.tsx +4 -0
- package/src/index.web.tsx +4 -0
|
@@ -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
|
|
@@ -8,6 +8,25 @@
|
|
|
8
8
|
|
|
9
9
|
using namespace facebook::react;
|
|
10
10
|
|
|
11
|
+
static void RNCNativeScrollerHomeVisualLog(NSString *phase, UIScrollView *scrollView)
|
|
12
|
+
{
|
|
13
|
+
#if DEBUG
|
|
14
|
+
Class logClass = NSClassFromString(@"ReactNativeNativeLogger.OneKeyLog");
|
|
15
|
+
if (logClass == nil) logClass = NSClassFromString(@"OneKeyLog");
|
|
16
|
+
SEL selector = NSSelectorFromString(@"debug::");
|
|
17
|
+
if (logClass == nil || ![logClass respondsToSelector:selector]) return;
|
|
18
|
+
UIRefreshControl *control = scrollView.refreshControl;
|
|
19
|
+
NSString *message = [NSString stringWithFormat:
|
|
20
|
+
@"[IOS-HOME-VISUAL] epochMs=%.0f monoMs=%.0f phase=scroller-%@ scrollTag=%ld refresh=%d refreshing=%d scrollY=%.1f insetTop=%.1f",
|
|
21
|
+
[NSDate.date timeIntervalSince1970] * 1000, CACurrentMediaTime() * 1000,
|
|
22
|
+
phase, (long)scrollView.tag, control != nil, control.isRefreshing,
|
|
23
|
+
scrollView.contentOffset.y, scrollView.contentInset.top];
|
|
24
|
+
typedef void (*LogFunction)(id, SEL, NSString *, NSString *);
|
|
25
|
+
LogFunction logFunction = (LogFunction)[logClass methodForSelector:selector];
|
|
26
|
+
logFunction(logClass, selector, @"NativeScroller", message);
|
|
27
|
+
#endif
|
|
28
|
+
}
|
|
29
|
+
|
|
11
30
|
@interface RNCNativeVerticalScrollView : UIScrollView
|
|
12
31
|
@end
|
|
13
32
|
@implementation RNCNativeVerticalScrollView
|
|
@@ -156,6 +175,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
|
|
|
156
175
|
[_refreshControl addTarget:self action:@selector(refreshTriggered:)
|
|
157
176
|
forControlEvents:UIControlEventValueChanged];
|
|
158
177
|
_scrollView.refreshControl = _refreshControl;
|
|
178
|
+
RNCNativeScrollerHomeVisualLog(@"refresh-created", _scrollView);
|
|
159
179
|
} else if (!next.refreshEnabled && _refreshControl != nil) {
|
|
160
180
|
[_refreshControl endRefreshing];
|
|
161
181
|
_scrollView.refreshControl = nil;
|
|
@@ -174,8 +194,10 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
|
|
|
174
194
|
}
|
|
175
195
|
if (next.refreshing && !_refreshControl.refreshing) {
|
|
176
196
|
[_refreshControl beginRefreshing];
|
|
197
|
+
RNCNativeScrollerHomeVisualLog(@"refresh-begin-prop", _scrollView);
|
|
177
198
|
} else if (!next.refreshing && (previous.refreshing || createdRefreshControl)) {
|
|
178
199
|
[_refreshControl endRefreshing];
|
|
200
|
+
if (previous.refreshing) RNCNativeScrollerHomeVisualLog(@"refresh-end-prop", _scrollView);
|
|
179
201
|
}
|
|
180
202
|
}
|
|
181
203
|
|
|
@@ -276,10 +298,23 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
|
|
|
276
298
|
}
|
|
277
299
|
}
|
|
278
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
|
+
|
|
279
313
|
- (void)didMoveToSuperview
|
|
280
314
|
{
|
|
281
315
|
[super didMoveToSuperview];
|
|
282
316
|
[self updateTopScrollEdge];
|
|
317
|
+
[self notifyCoordinatorScrollViewReadyIfNeeded];
|
|
283
318
|
}
|
|
284
319
|
|
|
285
320
|
- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
|
|
@@ -292,6 +327,10 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
|
|
|
292
327
|
if (!CGSizeEqualToSize(previous, _scrollView.bounds.size)) {
|
|
293
328
|
_needsContentClamp = YES;
|
|
294
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
|
+
}
|
|
295
334
|
}
|
|
296
335
|
}
|
|
297
336
|
|
|
@@ -379,6 +418,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
|
|
|
379
418
|
const auto &props = *std::static_pointer_cast<const RNCNativeScrollerProps>(_props);
|
|
380
419
|
if (!props.refreshEnabled || props.refreshing) return;
|
|
381
420
|
_refreshEventSentForDrag = YES;
|
|
421
|
+
RNCNativeScrollerHomeVisualLog(@"refresh-native-trigger", _scrollView);
|
|
382
422
|
if (_eventEmitter) {
|
|
383
423
|
std::static_pointer_cast<const RNCNativeScrollerEventEmitter>(_eventEmitter)->onRefresh({});
|
|
384
424
|
}
|
|
@@ -523,6 +563,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
|
|
|
523
563
|
- (void)setRefreshing:(BOOL)refreshing
|
|
524
564
|
{
|
|
525
565
|
if (_recycled || _refreshControl == nil) return;
|
|
566
|
+
RNCNativeScrollerHomeVisualLog(refreshing ? @"refresh-command-start" : @"refresh-command-stop", _scrollView);
|
|
526
567
|
if (refreshing) {
|
|
527
568
|
if (!_refreshControl.refreshing) [_refreshControl beginRefreshing];
|
|
528
569
|
} else {
|
|
@@ -550,6 +591,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
|
|
|
550
591
|
const auto &props = *std::static_pointer_cast<const RNCNativeScrollerProps>(_props);
|
|
551
592
|
if (props.refreshing) [_refreshControl beginRefreshing];
|
|
552
593
|
[self scheduleContentClamp];
|
|
594
|
+
[self notifyCoordinatorScrollViewReadyIfNeeded];
|
|
553
595
|
}
|
|
554
596
|
}
|
|
555
597
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#import <XCTest/XCTest.h>
|
|
2
2
|
#import "RNCCollapsiblePagerViewComponentView.h"
|
|
3
3
|
#import "RNCNativeScrollerComponentView.h"
|
|
4
|
+
#import <React/RCTScrollViewComponentView.h>
|
|
5
|
+
#import <react/renderer/components/pagerview/Props.h>
|
|
4
6
|
#import <react/renderer/mounting/MountingTransaction.h>
|
|
5
7
|
|
|
6
8
|
using namespace facebook::react;
|
|
@@ -12,6 +14,7 @@ using namespace facebook::react;
|
|
|
12
14
|
withSurfaceTelemetry:(const SurfaceTelemetry &)telemetry;
|
|
13
15
|
- (UIScrollView *)findExplicitNativeScrollerInView:(UIView *)view;
|
|
14
16
|
- (UIScrollView *)findVerticalScrollViewInView:(UIView *)view;
|
|
17
|
+
- (UIScrollView *)verticalScrollViewForPageAtIndex:(NSInteger)pageIndex;
|
|
15
18
|
- (void)attachScrollObserverForCurrentPage;
|
|
16
19
|
- (void)attachScrollObserverForCurrentPageAfterTransition:(BOOL)selectionSettled;
|
|
17
20
|
- (void)detachScrollObserver;
|
|
@@ -22,6 +25,10 @@ using namespace facebook::react;
|
|
|
22
25
|
- (void)updateHeaderSettledStateForceBusy:(BOOL)forceBusy;
|
|
23
26
|
- (void)preparePageForHorizontalTransitionAtIndex:(NSInteger)index;
|
|
24
27
|
- (void)applyInsetsToScrollView:(UIScrollView *)scrollView pageIndex:(NSInteger)index restore:(BOOL)restore;
|
|
28
|
+
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps;
|
|
29
|
+
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index;
|
|
30
|
+
- (CGFloat)effectiveTopOverlayHeight;
|
|
31
|
+
- (void)updateRefreshControlForScrollView:(UIScrollView *)scrollView;
|
|
25
32
|
@end
|
|
26
33
|
|
|
27
34
|
@interface RNCNativeScrollerComponentView (ScrollCommandTesting)
|
|
@@ -34,6 +41,125 @@ using namespace facebook::react;
|
|
|
34
41
|
|
|
35
42
|
@implementation RNCCollapsiblePagerFirstFrameTests
|
|
36
43
|
|
|
44
|
+
- (void)testPendingLocalOverlayReservesManagedScrollerInsetBeforeSlotRebuild
|
|
45
|
+
{
|
|
46
|
+
RNCCollapsiblePagerViewComponentView *pager =
|
|
47
|
+
[[RNCCollapsiblePagerViewComponentView alloc] initWithFrame:CGRectMake(0, 0, 320, 844)];
|
|
48
|
+
UIWindow *window = [[UIWindow alloc] initWithFrame:pager.frame];
|
|
49
|
+
UIViewController *root = [UIViewController new];
|
|
50
|
+
window.rootViewController = root;
|
|
51
|
+
[root.view addSubview:pager];
|
|
52
|
+
window.hidden = NO;
|
|
53
|
+
[self addTeardownBlock:^{
|
|
54
|
+
[pager prepareForRecycle];
|
|
55
|
+
window.hidden = YES;
|
|
56
|
+
}];
|
|
57
|
+
|
|
58
|
+
auto baseline = std::make_shared<RNCCollapsiblePagerViewProps>();
|
|
59
|
+
baseline->nativeSmoothHeaderScrollEnabled = true;
|
|
60
|
+
baseline->headerHeight = 120;
|
|
61
|
+
baseline->stickyHeaderHeight = 44;
|
|
62
|
+
baseline->pageKeys = "[\"page-0\"]";
|
|
63
|
+
[pager updateProps:baseline oldProps:nullptr];
|
|
64
|
+
|
|
65
|
+
UIView *header = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 120)];
|
|
66
|
+
UIView *sticky = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 44)];
|
|
67
|
+
UIView *page = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 844)];
|
|
68
|
+
RNCNativeScrollerComponentView *scroller = [RNCNativeScrollerComponentView new];
|
|
69
|
+
scroller.frame = page.bounds;
|
|
70
|
+
scroller.scrollView.frame = scroller.bounds;
|
|
71
|
+
scroller.scrollView.contentSize = CGSizeMake(320, 1500);
|
|
72
|
+
[page addSubview:scroller];
|
|
73
|
+
[pager mountChildComponentView:(id)header index:0];
|
|
74
|
+
[pager mountChildComponentView:(id)sticky index:1];
|
|
75
|
+
[pager mountChildComponentView:(id)page index:2];
|
|
76
|
+
[root.view layoutIfNeeded];
|
|
77
|
+
[pager layoutIfNeeded];
|
|
78
|
+
XCTestExpectation *settled = [self expectationWithDescription:@"Baseline slots and observer settled"];
|
|
79
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [settled fulfill]; });
|
|
80
|
+
[self waitForExpectations:@[settled] timeout:1];
|
|
81
|
+
[pager attachScrollObserverForCurrentPage];
|
|
82
|
+
UIScrollView *ownedScroll = scroller.scrollView;
|
|
83
|
+
XCTAssertEqual([pager valueForKey:@"observedScrollView"], ownedScroll);
|
|
84
|
+
XCTAssertEqualWithAccuracy(ownedScroll.contentInset.top, 164, 0.01);
|
|
85
|
+
[scroller scrollTo:0 y:40 - ownedScroll.contentInset.top animated:NO];
|
|
86
|
+
[pager setValue:@40 forKey:@"headerOffset"];
|
|
87
|
+
CGFloat initialLogicalOffset = ownedScroll.contentOffset.y + ownedScroll.contentInset.top;
|
|
88
|
+
XCTAssertEqualWithAccuracy(initialLogicalOffset, 40, 0.01);
|
|
89
|
+
|
|
90
|
+
auto hard = std::make_shared<RNCCollapsiblePagerViewProps>();
|
|
91
|
+
hard->nativeSmoothHeaderScrollEnabled = true;
|
|
92
|
+
hard->headerHeight = 120;
|
|
93
|
+
hard->stickyHeaderHeight = 44;
|
|
94
|
+
hard->pageKeys = "[\"page-0\"]";
|
|
95
|
+
hard->topScrollEdgeEffect = RNCCollapsiblePagerViewTopScrollEdgeEffect::Hard;
|
|
96
|
+
hard->topOverlayHeight = 162;
|
|
97
|
+
[pager updateProps:hard oldProps:baseline];
|
|
98
|
+
XCTAssertNil([pager valueForKey:@"topOverlayView"]);
|
|
99
|
+
XCTAssertEqual([pager valueForKey:@"observedScrollView"], ownedScroll);
|
|
100
|
+
XCTAssertEqualWithAccuracy([pager effectiveTopOverlayHeight], 162, 0.01);
|
|
101
|
+
XCTAssertEqualWithAccuracy(ownedScroll.contentInset.top, 326, 0.01);
|
|
102
|
+
XCTAssertEqualWithAccuracy(ownedScroll.contentOffset.y + ownedScroll.contentInset.top,
|
|
103
|
+
initialLogicalOffset, 0.01);
|
|
104
|
+
[self finishMount:pager];
|
|
105
|
+
XCTAssertNil([pager valueForKey:@"topOverlayView"]);
|
|
106
|
+
XCTAssertTrue([[pager valueForKey:@"deferTopEdgeSlotRebuild"] boolValue]);
|
|
107
|
+
|
|
108
|
+
UIView *overlay = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 162)];
|
|
109
|
+
[pager mountChildComponentView:(id)overlay index:2];
|
|
110
|
+
XCTAssertNil([pager valueForKey:@"topOverlayView"]);
|
|
111
|
+
[self finishMount:pager];
|
|
112
|
+
UIView *overlayHost = [pager valueForKey:@"topOverlayHostView"];
|
|
113
|
+
XCTAssertEqual([pager valueForKey:@"topOverlayView"], overlay);
|
|
114
|
+
XCTAssertEqual(overlay.superview, overlayHost);
|
|
115
|
+
XCTAssertEqual(overlay.window, window);
|
|
116
|
+
XCTAssertEqualWithAccuracy(ownedScroll.contentInset.top, 326, 0.01);
|
|
117
|
+
XCTAssertEqualWithAccuracy(ownedScroll.contentOffset.y + ownedScroll.contentInset.top,
|
|
118
|
+
initialLogicalOffset, 0.01);
|
|
119
|
+
XCTestExpectation *rebuilt = [self expectationWithDescription:@"Post-mount observer restored"];
|
|
120
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [rebuilt fulfill]; });
|
|
121
|
+
[self waitForExpectations:@[rebuilt] timeout:1];
|
|
122
|
+
[pager attachScrollObserverForCurrentPage];
|
|
123
|
+
XCTAssertEqual([pager valueForKey:@"topOverlayView"], overlay);
|
|
124
|
+
XCTAssertEqual([pager valueForKey:@"observedScrollView"], ownedScroll);
|
|
125
|
+
XCTAssertEqualWithAccuracy(ownedScroll.contentInset.top, 326, 0.01);
|
|
126
|
+
XCTAssertEqualWithAccuracy(ownedScroll.contentOffset.y + ownedScroll.contentInset.top,
|
|
127
|
+
initialLogicalOffset, 0.01);
|
|
128
|
+
|
|
129
|
+
UIRefreshControl *control = [UIRefreshControl new];
|
|
130
|
+
ownedScroll.refreshControl = control;
|
|
131
|
+
[pager updateRefreshControlForScrollView:ownedScroll];
|
|
132
|
+
UIEdgeInsets refreshingInset = ownedScroll.contentInset;
|
|
133
|
+
refreshingInset.top += 60;
|
|
134
|
+
ownedScroll.contentInset = refreshingInset;
|
|
135
|
+
auto disabled = std::make_shared<RNCCollapsiblePagerViewProps>();
|
|
136
|
+
disabled->nativeSmoothHeaderScrollEnabled = true;
|
|
137
|
+
disabled->headerHeight = 120;
|
|
138
|
+
disabled->stickyHeaderHeight = 44;
|
|
139
|
+
disabled->pageKeys = "[\"page-0\"]";
|
|
140
|
+
disabled->topScrollEdgeEffect = RNCCollapsiblePagerViewTopScrollEdgeEffect::None;
|
|
141
|
+
disabled->topOverlayHeight = 0;
|
|
142
|
+
[pager updateProps:disabled oldProps:hard];
|
|
143
|
+
XCTAssertEqual(ownedScroll.refreshControl, control);
|
|
144
|
+
XCTAssertEqualWithAccuracy([pager effectiveTopOverlayHeight], 0, 0.01);
|
|
145
|
+
XCTAssertEqualWithAccuracy(ownedScroll.contentInset.top, 224, 0.01);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
- (void)testPendingLocalOverlayHeightDoesNotOverrideExternalGeometry
|
|
149
|
+
{
|
|
150
|
+
RNCCollapsiblePagerViewComponentView *pager = [RNCCollapsiblePagerViewComponentView new];
|
|
151
|
+
auto props = std::make_shared<RNCCollapsiblePagerViewProps>();
|
|
152
|
+
props->topScrollEdgeEffect = RNCCollapsiblePagerViewTopScrollEdgeEffect::Hard;
|
|
153
|
+
props->topOverlayHeight = 162;
|
|
154
|
+
props->externalTopOverlayTag = 999;
|
|
155
|
+
[pager updateProps:props oldProps:nullptr];
|
|
156
|
+
XCTAssertNil([pager valueForKey:@"topOverlayView"]);
|
|
157
|
+
XCTAssertEqualWithAccuracy([pager effectiveTopOverlayHeight], 0, 0.01);
|
|
158
|
+
[pager setValue:@64 forKey:@"externalTopOverlayHeight"];
|
|
159
|
+
XCTAssertEqualWithAccuracy([pager effectiveTopOverlayHeight], 64, 0.01);
|
|
160
|
+
[pager prepareForRecycle];
|
|
161
|
+
}
|
|
162
|
+
|
|
37
163
|
- (void)testHeaderSettledAtTopRequiresCurrentQuietOwnerAndCancelsDetachedTimer
|
|
38
164
|
{
|
|
39
165
|
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
|
|
@@ -335,6 +461,138 @@ using namespace facebook::react;
|
|
|
335
461
|
XCTAssertTrue([[pager valueForKey:@"pendingPageRestoreKeys"] containsObject:@"page-1"]);
|
|
336
462
|
}
|
|
337
463
|
|
|
464
|
+
- (void)testReleasedOwnerRecoversReplacementWithoutAnotherFabricInsertion
|
|
465
|
+
{
|
|
466
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
|
|
467
|
+
// The fixture mounts page 0 directly; UIKit must not select it again.
|
|
468
|
+
[pager setValue:@YES forKey:@"hasAppliedInitialPage"];
|
|
469
|
+
RNCNativeScrollerComponentView *source = [self addScrollerToPager:pager atIndex:0];
|
|
470
|
+
source.tag = 91001;
|
|
471
|
+
[pager.window layoutIfNeeded];
|
|
472
|
+
[pager layoutIfNeeded];
|
|
473
|
+
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.15]];
|
|
474
|
+
[pager setValue:@120 forKey:@"headerOffset"];
|
|
475
|
+
[pager attachScrollObserverForCurrentPage];
|
|
476
|
+
XCTAssertEqual([pager valueForKey:@"observedScrollView"], source.scrollView);
|
|
477
|
+
|
|
478
|
+
// Finish the old owner's removal before the replacement becomes available.
|
|
479
|
+
ShadowView removedView;
|
|
480
|
+
removedView.tag = (Tag)source.tag;
|
|
481
|
+
MountingTransaction removal(1, 1, {ShadowViewMutation::RemoveMutation(1, removedView, 0)}, {});
|
|
482
|
+
SurfaceTelemetry telemetry;
|
|
483
|
+
[pager mountingTransactionWillMount:removal withSurfaceTelemetry:telemetry];
|
|
484
|
+
[source removeFromSuperview];
|
|
485
|
+
[self finishMount:pager];
|
|
486
|
+
XCTAssertNil([pager valueForKey:@"observedScrollView"]);
|
|
487
|
+
|
|
488
|
+
RNCNativeScrollerComponentView *replacement = [self addScrollerToPager:pager atIndex:0];
|
|
489
|
+
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]];
|
|
490
|
+
XCTAssertEqual([pager valueForKey:@"observedScrollView"], replacement.scrollView);
|
|
491
|
+
XCTAssertEqual([pager valueForKey:@"sharedHeaderScrollView"], replacement.scrollView);
|
|
492
|
+
XCTAssertEqualWithAccuracy(replacement.scrollView.contentOffset.y + replacement.scrollView.contentInset.top,
|
|
493
|
+
120, 0.01);
|
|
494
|
+
XCTAssertEqualWithAccuracy([[pager valueForKey:@"headerOffset"] doubleValue], 120, 0.01);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
- (void)testCurrentPageInsertionRestartsExhaustedReplacementRecovery
|
|
498
|
+
{
|
|
499
|
+
[self assertReplacementRecoveryWithCancelledDrag:NO finalRetryPending:NO];
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
- (void)testCancelledDragRetainsReplacementRecoveryFromInsertion
|
|
503
|
+
{
|
|
504
|
+
[self assertReplacementRecoveryWithCancelledDrag:YES finalRetryPending:NO];
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
- (void)testInsertionInvalidatesPendingFinalReplacementRetry
|
|
508
|
+
{
|
|
509
|
+
[self assertReplacementRecoveryWithCancelledDrag:NO finalRetryPending:YES];
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
- (void)assertReplacementRecoveryWithCancelledDrag:(BOOL)cancelDrag
|
|
513
|
+
finalRetryPending:(BOOL)finalRetryPending
|
|
514
|
+
{
|
|
515
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:2];
|
|
516
|
+
[pager setValue:@YES forKey:@"hasAppliedInitialPage"];
|
|
517
|
+
RNCNativeScrollerComponentView *source = [self addScrollerToPager:pager atIndex:0];
|
|
518
|
+
source.tag = 91001;
|
|
519
|
+
UIView *page = source.superview;
|
|
520
|
+
page.tag = 92001;
|
|
521
|
+
UIViewController *otherPage = [pager valueForKey:@"pageControllers"][1];
|
|
522
|
+
otherPage.view.tag = 92002;
|
|
523
|
+
[pager.window layoutIfNeeded];
|
|
524
|
+
[pager layoutIfNeeded];
|
|
525
|
+
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.15]];
|
|
526
|
+
[pager setValue:@120 forKey:@"headerOffset"];
|
|
527
|
+
[pager attachScrollObserverForCurrentPage];
|
|
528
|
+
XCTAssertEqual([pager valueForKey:@"observedScrollView"], source.scrollView);
|
|
529
|
+
|
|
530
|
+
ShadowView removedView;
|
|
531
|
+
removedView.tag = (Tag)source.tag;
|
|
532
|
+
MountingTransaction removal(1, 1, {ShadowViewMutation::RemoveMutation((Tag)page.tag, removedView, 0)}, {});
|
|
533
|
+
SurfaceTelemetry telemetry;
|
|
534
|
+
[pager mountingTransactionWillMount:removal withSurfaceTelemetry:telemetry];
|
|
535
|
+
[source removeFromSuperview];
|
|
536
|
+
[self finishMount:pager];
|
|
537
|
+
NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:3];
|
|
538
|
+
while (([[pager valueForKey:@"scrollResolveRetryAttempt"] integerValue] < 12 ||
|
|
539
|
+
(!finalRetryPending && [[pager valueForKey:@"scrollResolveRetryScheduled"] boolValue])) &&
|
|
540
|
+
deadline.timeIntervalSinceNow > 0) {
|
|
541
|
+
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];
|
|
542
|
+
}
|
|
543
|
+
XCTAssertEqualObjects([pager valueForKey:@"scrollResolveRetryAttempt"], @12);
|
|
544
|
+
XCTAssertEqualObjects([pager valueForKey:@"scrollResolveRetryScheduled"], @(finalRetryPending));
|
|
545
|
+
XCTAssertNotNil([pager valueForKey:@"releasedPageScrollStates"][@"page-0"]);
|
|
546
|
+
|
|
547
|
+
// Only a new insertion in the current page can reopen the exhausted budget.
|
|
548
|
+
[self finishMount:pager];
|
|
549
|
+
[pager mountingTransactionDidMount:removal withSurfaceTelemetry:telemetry];
|
|
550
|
+
UIView *replacementHost = [UIView new];
|
|
551
|
+
replacementHost.tag = 93001;
|
|
552
|
+
[otherPage.view addSubview:replacementHost];
|
|
553
|
+
ShadowView insertedView;
|
|
554
|
+
insertedView.tag = (Tag)replacementHost.tag;
|
|
555
|
+
MountingTransaction unrelated(1, 2, {ShadowViewMutation::InsertMutation((Tag)otherPage.view.tag, insertedView, 0)}, {});
|
|
556
|
+
[pager mountingTransactionDidMount:unrelated withSurfaceTelemetry:telemetry];
|
|
557
|
+
XCTAssertEqualObjects([pager valueForKey:@"scrollResolveRetryAttempt"], @12);
|
|
558
|
+
XCTAssertEqualObjects([pager valueForKey:@"scrollResolveRetryScheduled"], @(finalRetryPending));
|
|
559
|
+
|
|
560
|
+
if (cancelDrag) {
|
|
561
|
+
[pager beginPageStateTransition];
|
|
562
|
+
[pager setValue:@YES forKey:@"transitioning"];
|
|
563
|
+
[pager setValue:@YES forKey:@"isPagerDragging"];
|
|
564
|
+
}
|
|
565
|
+
[page addSubview:replacementHost];
|
|
566
|
+
MountingTransaction insertion(1, 3, {ShadowViewMutation::InsertMutation((Tag)page.tag, insertedView, 0)}, {});
|
|
567
|
+
[pager mountingTransactionDidMount:insertion withSurfaceTelemetry:telemetry];
|
|
568
|
+
XCTAssertNil([pager valueForKey:@"observedScrollView"]);
|
|
569
|
+
NSNumber *retryToken = [pager valueForKey:@"scrollResolveRetryToken"];
|
|
570
|
+
[pager mountingTransactionDidMount:insertion withSurfaceTelemetry:telemetry];
|
|
571
|
+
XCTAssertEqualObjects([pager valueForKey:@"scrollResolveRetryToken"], retryToken);
|
|
572
|
+
if (cancelDrag) {
|
|
573
|
+
XCTAssertNil([pager valueForKey:@"sharedHeaderScrollView"]);
|
|
574
|
+
XCTAssertFalse([[pager valueForKey:@"scrollResolveRetryScheduled"] boolValue]);
|
|
575
|
+
// Settle on the source page while the replacement is still unavailable.
|
|
576
|
+
[pager attachScrollObserverForCurrentPageAfterTransition:YES];
|
|
577
|
+
[pager setValue:@NO forKey:@"isPagerDragging"];
|
|
578
|
+
[pager setValue:@NO forKey:@"transitioning"];
|
|
579
|
+
[pager attachScrollObserverForCurrentPage];
|
|
580
|
+
XCTAssertEqualObjects([pager valueForKey:@"currentIndex"], @0);
|
|
581
|
+
}
|
|
582
|
+
__block RNCNativeScrollerComponentView *replacement;
|
|
583
|
+
int64_t replacementDelay = finalRetryPending ? 150 : 50;
|
|
584
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, replacementDelay * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
|
|
585
|
+
replacement = [self addScrollerToPager:pager atIndex:0];
|
|
586
|
+
[replacementHost addSubview:replacement];
|
|
587
|
+
});
|
|
588
|
+
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]];
|
|
589
|
+
XCTAssertEqual([pager valueForKey:@"observedScrollView"], replacement.scrollView);
|
|
590
|
+
XCTAssertEqual([pager valueForKey:@"sharedHeaderScrollView"], replacement.scrollView);
|
|
591
|
+
XCTAssertEqualWithAccuracy(replacement.scrollView.contentOffset.y + replacement.scrollView.contentInset.top,
|
|
592
|
+
120, 0.01);
|
|
593
|
+
XCTAssertEqualWithAccuracy([[pager valueForKey:@"headerOffset"] doubleValue], 120, 0.01);
|
|
594
|
+
}
|
|
595
|
+
|
|
338
596
|
- (void)testRenamedPageDoesNotResurrectPrunedObserverCache
|
|
339
597
|
{
|
|
340
598
|
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
|
|
@@ -479,4 +737,139 @@ using namespace facebook::react;
|
|
|
479
737
|
XCTAssertEqual(removed.count, 0u);
|
|
480
738
|
}
|
|
481
739
|
|
|
740
|
+
- (void)testEmptyPageWithoutReleasedOwnerOrScrollHostDoesNotScheduleRetry
|
|
741
|
+
{
|
|
742
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
|
|
743
|
+
[pager attachScrollObserverForCurrentPage];
|
|
744
|
+
XCTAssertNil([pager valueForKey:@"observedScrollView"]);
|
|
745
|
+
XCTAssertFalse([[pager valueForKey:@"scrollResolveRetryScheduled"] boolValue]);
|
|
746
|
+
XCTAssertEqual([[pager valueForKey:@"scrollResolveRetryAttempt"] integerValue], 0);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
- (void)testLateReplacementRecoversImmediatelyOnReadinessEvenAfterRetryBudgetExpires
|
|
750
|
+
{
|
|
751
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
|
|
752
|
+
RNCNativeScrollerComponentView *previous = [self addScrollerToPager:pager atIndex:0];
|
|
753
|
+
previous.tag = 91003;
|
|
754
|
+
[pager attachScrollObserverForCurrentPage];
|
|
755
|
+
UIView *sharedHeader = [pager valueForKey:@"sharedHeaderHostView"];
|
|
756
|
+
XCTAssertEqual([pager valueForKey:@"observedScrollView"], previous.scrollView);
|
|
757
|
+
|
|
758
|
+
ShadowView removedView;
|
|
759
|
+
removedView.tag = (Tag)previous.tag;
|
|
760
|
+
MountingTransaction removal(1, 1, {ShadowViewMutation::RemoveMutation(1, removedView, 0)}, {});
|
|
761
|
+
SurfaceTelemetry telemetry;
|
|
762
|
+
[pager mountingTransactionWillMount:removal withSurfaceTelemetry:telemetry];
|
|
763
|
+
[previous removeFromSuperview];
|
|
764
|
+
[self finishMount:pager];
|
|
765
|
+
XCTAssertNil([pager valueForKey:@"observedScrollView"]);
|
|
766
|
+
|
|
767
|
+
// Simulate retry budget exhaustion before the replacement becomes ready.
|
|
768
|
+
[pager setValue:@12 forKey:@"scrollResolveRetryAttempt"];
|
|
769
|
+
[pager setValue:@NO forKey:@"scrollResolveRetryScheduled"];
|
|
770
|
+
NSUInteger oldToken = [[pager valueForKey:@"scrollResolveRetryToken"] unsignedIntegerValue];
|
|
771
|
+
[pager setValue:@(oldToken + 1) forKey:@"scrollResolveRetryToken"];
|
|
772
|
+
|
|
773
|
+
UIViewController *page = [pager valueForKey:@"pageControllers"][0];
|
|
774
|
+
UICollectionView *lateList = [[UICollectionView alloc]
|
|
775
|
+
initWithFrame:CGRectMake(0, 0, 320, 600)
|
|
776
|
+
collectionViewLayout:[UICollectionViewFlowLayout new]];
|
|
777
|
+
[page.view addSubview:lateList];
|
|
778
|
+
|
|
779
|
+
// Readiness notification triggers immediate reconcile even after retry budget expired.
|
|
780
|
+
[[NSNotificationCenter defaultCenter]
|
|
781
|
+
postNotificationName:@"OneKeyNativeScrollViewDidBecomeReady"
|
|
782
|
+
object:lateList];
|
|
783
|
+
XCTAssertEqualObjects([pager valueForKey:@"pendingFallbackScrollView"], lateList);
|
|
784
|
+
|
|
785
|
+
// Explicit scroller readiness attaches synchronously with zero debounce delay.
|
|
786
|
+
[lateList removeFromSuperview];
|
|
787
|
+
RNCNativeScrollerComponentView *lateScroller = [self addScrollerToPager:pager atIndex:0];
|
|
788
|
+
XCTAssertEqual([pager valueForKey:@"observedScrollView"], lateScroller.scrollView);
|
|
789
|
+
XCTAssertEqual(sharedHeader.superview, lateScroller.scrollView);
|
|
790
|
+
XCTAssertEqualWithAccuracy(lateScroller.scrollView.contentInset.top, 164, 0.01);
|
|
791
|
+
XCTAssertNil([pager valueForKey:@"releasedPageScrollStates"][@"page-0"]);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
- (RCTViewComponentView *)addNativeListToPager:(RNCCollapsiblePagerViewComponentView *)pager
|
|
795
|
+
{
|
|
796
|
+
Class nativeListClass = NSClassFromString(@"HybridNativeListComponent");
|
|
797
|
+
XCTAssertNotNil(nativeListClass, @"The real NativeList test dependency must be linked");
|
|
798
|
+
RCTViewComponentView *component = [[nativeListClass alloc] init];
|
|
799
|
+
component.tag = 92001;
|
|
800
|
+
component.frame = CGRectMake(0, 0, 320, 600);
|
|
801
|
+
component.contentView.frame = component.bounds;
|
|
802
|
+
UIViewController *page = [pager valueForKey:@"pageControllers"][0];
|
|
803
|
+
[page.view addSubview:component];
|
|
804
|
+
[component.contentView layoutIfNeeded];
|
|
805
|
+
return component;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
- (void)testNativeListFooterScrollViewCannotReplacePageOwner
|
|
809
|
+
{
|
|
810
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
|
|
811
|
+
RCTViewComponentView *component = [self addNativeListToPager:pager];
|
|
812
|
+
UICollectionView *list = nil;
|
|
813
|
+
for (UIView *child in component.contentView.subviews) {
|
|
814
|
+
if ([child isKindOfClass:UICollectionView.class]) {
|
|
815
|
+
list = (UICollectionView *)child;
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
XCTAssertTrue([list isKindOfClass:UICollectionView.class]);
|
|
820
|
+
XCTAssertGreaterThan(CGRectGetWidth(list.bounds), 0);
|
|
821
|
+
XCTAssertGreaterThan(CGRectGetHeight(list.bounds), 0);
|
|
822
|
+
XCTAssertEqual(list.delegate, (id)component.contentView);
|
|
823
|
+
XCTAssertTrue([list isDescendantOfView:component.contentView]);
|
|
824
|
+
[pager attachScrollObserverForCurrentPage];
|
|
825
|
+
UIView *header = [pager valueForKey:@"sharedHeaderHostView"];
|
|
826
|
+
XCTAssertNotNil(header);
|
|
827
|
+
XCTAssertEqual([pager valueForKey:@"observedScrollView"], list);
|
|
828
|
+
XCTAssertEqual(header.superview, list);
|
|
829
|
+
CGFloat inset = list.contentInset.top;
|
|
830
|
+
CGFloat offset = list.contentOffset.y;
|
|
831
|
+
|
|
832
|
+
RCTScrollViewComponentView *footerList = [RCTScrollViewComponentView new];
|
|
833
|
+
footerList.tag = 92002;
|
|
834
|
+
footerList.frame = CGRectMake(0, 0, 320, 400);
|
|
835
|
+
footerList.scrollView.frame = footerList.bounds;
|
|
836
|
+
footerList.scrollView.contentSize = CGSizeMake(320, 1000);
|
|
837
|
+
footerList.scrollView.alwaysBounceHorizontal = NO;
|
|
838
|
+
for (NSInteger index = 0; index < 6; index++) {
|
|
839
|
+
UIView *slot = index == 2 ? footerList : [UIView new];
|
|
840
|
+
[component mountChildComponentView:(id)slot index:index];
|
|
841
|
+
}
|
|
842
|
+
XCTAssertTrue([footerList isDescendantOfView:list]);
|
|
843
|
+
XCTAssertEqual([pager verticalScrollViewForPageAtIndex:0], list);
|
|
844
|
+
ShadowView inserted;
|
|
845
|
+
inserted.tag = (Tag)footerList.tag;
|
|
846
|
+
MountingTransaction transaction(1, 2,
|
|
847
|
+
{ShadowViewMutation::InsertMutation((Tag)component.tag, inserted, 2)}, {});
|
|
848
|
+
SurfaceTelemetry telemetry;
|
|
849
|
+
[pager mountingTransactionDidMount:transaction withSurfaceTelemetry:telemetry];
|
|
850
|
+
XCTAssertEqual([pager valueForKey:@"observedScrollView"], list);
|
|
851
|
+
XCTAssertEqual(header.superview, list);
|
|
852
|
+
XCTAssertEqualWithAccuracy(list.contentInset.top, inset, 0.01);
|
|
853
|
+
XCTAssertEqualWithAccuracy(list.contentOffset.y, offset, 0.01);
|
|
854
|
+
XCTAssertEqualWithAccuracy(footerList.scrollView.contentInset.top, 0, 0.01);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
- (void)testExplicitScrollerSiblingStillPrecedesNativeList
|
|
858
|
+
{
|
|
859
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
|
|
860
|
+
[self addNativeListToPager:pager];
|
|
861
|
+
RNCNativeScrollerComponentView *scroller = [self addScrollerToPager:pager atIndex:0];
|
|
862
|
+
XCTAssertEqual([pager verticalScrollViewForPageAtIndex:0], scroller.scrollView);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
- (void)testPageWithoutNativeListStillDelegatesToNestedPager
|
|
866
|
+
{
|
|
867
|
+
RNCCollapsiblePagerViewComponentView *outer = [self pagerWithPageCount:1];
|
|
868
|
+
RNCCollapsiblePagerViewComponentView *inner = [self pagerWithPageCount:1];
|
|
869
|
+
RNCNativeScrollerComponentView *scroller = [self addScrollerToPager:inner atIndex:0];
|
|
870
|
+
UIViewController *page = [outer valueForKey:@"pageControllers"][0];
|
|
871
|
+
[page.view addSubview:inner];
|
|
872
|
+
XCTAssertEqual([outer verticalScrollViewForPageAtIndex:0], scroller.scrollView);
|
|
873
|
+
}
|
|
874
|
+
|
|
482
875
|
@end
|