@onekeyfe/react-native-pager-view 3.0.173 → 3.0.174

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.
@@ -10,6 +10,7 @@ NS_ASSUME_NONNULL_BEGIN
10
10
 
11
11
  // Native inset ownership stays separate from Fabric/Yoga content layout.
12
12
  @protocol RNCNativeScrollerInsetCoordinator <NSObject>
13
+ - (void)nativeScrollerWillReceiveScrollCommand:(RNCNativeScrollerComponentView *)scroller;
13
14
  - (void)nativeScroller:(RNCNativeScrollerComponentView *)scroller
14
15
  callerInsetChangedFrom:(UIEdgeInsets)previous to:(UIEdgeInsets)next;
15
16
  @end
@@ -33,6 +33,9 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
33
33
  @end
34
34
 
35
35
  @implementation RNCNativeScrollerComponentView {
36
+ #if DEBUG
37
+ NSUInteger _offsetDiagSequence;
38
+ #endif
36
39
  RNCNativeScrollerShadowNode::ConcreteState::Shared _state;
37
40
  RNCNativeVerticalScrollView *_scrollView;
38
41
  UIView<RCTComponentViewProtocol> *_reactContent;
@@ -51,6 +54,24 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
51
54
  NSNumber *_lastContentViewportHeight;
52
55
  }
53
56
 
57
+ - (void)offsetDiag:(NSString *)reason
58
+ {
59
+ #if DEBUG
60
+ Class logClass = NSClassFromString(@"ReactNativeNativeLogger.OneKeyLog") ?: NSClassFromString(@"OneKeyLog");
61
+ SEL selector = NSSelectorFromString(@"debug::");
62
+ if (logClass == nil || ![logClass respondsToSelector:selector]) return;
63
+ NSString *message = [NSString stringWithFormat:
64
+ @"[HOME-OFFSET-DIAG] scroller phase=%@ seq=%lu wallMs=%.0f mono=%.6f host=%p tag=%ld scroll=%p generation=%lu physicalY=%.4f insetTop=%.4f logicalY=%.4f contentHeight=%.4f boundsHeight=%.4f adjustedBottom=%.4f needsClamp=%d drag=%d decel=%d refresh=%d",
65
+ reason, (unsigned long)++_offsetDiagSequence, NSDate.date.timeIntervalSince1970 * 1000, CACurrentMediaTime(),
66
+ (__bridge void *)self, (long)self.tag, (__bridge void *)_scrollView, (unsigned long)_generation,
67
+ _scrollView.contentOffset.y, _scrollView.contentInset.top, _scrollView.contentOffset.y + _scrollView.contentInset.top,
68
+ _scrollView.contentSize.height, _scrollView.bounds.size.height, _scrollView.adjustedContentInset.bottom,
69
+ _needsContentClamp, _scrollView.dragging, _scrollView.decelerating, _refreshControl.refreshing];
70
+ typedef void (*LogFunction)(id, SEL, NSString *, NSString *);
71
+ ((LogFunction)[logClass methodForSelector:selector])(logClass, selector, @"NativeScroller", message);
72
+ #endif
73
+ }
74
+
54
75
  + (ComponentDescriptorProvider)componentDescriptorProvider
55
76
  {
56
77
  return concreteComponentDescriptorProvider<RNCNativeScrollerComponentDescriptor>();
@@ -179,7 +200,9 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
179
200
  _needsContentClamp |= size.height < _scrollView.contentSize.height;
180
201
  CGPoint offset = _scrollView.contentOffset;
181
202
  BOOL wasPulling = offset.y < -_scrollView.adjustedContentInset.top;
203
+ [self offsetDiag:@"content-size-before"];
182
204
  _scrollView.contentSize = size;
205
+ [self offsetDiag:@"content-size-after"];
183
206
  // UIKit can synchronously discard a pull when content becomes empty.
184
207
  // Upper-range shrink correction remains owned by the deferred clamp.
185
208
  if (wasPulling && _scrollView.contentOffset.y != offset.y) {
@@ -197,10 +220,12 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
197
220
  - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
198
221
  oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics
199
222
  {
223
+ [self offsetDiag:@"layout-before"];
200
224
  CGSize previous = _scrollView.bounds.size;
201
225
  [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
202
226
  [self updateFabricContentOrigin];
203
227
  [self emitContentViewportChangeIfNeeded];
228
+ [self offsetDiag:@"layout-after"];
204
229
  if (!CGSizeEqualToSize(previous, _scrollView.bounds.size)) {
205
230
  _needsContentClamp = YES;
206
231
  [self scheduleContentClamp];
@@ -263,6 +288,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
263
288
  - (void)scheduleContentClamp
264
289
  {
265
290
  if (!_needsContentClamp) return;
291
+ [self offsetDiag:@"clamp-scheduled"];
266
292
  NSUInteger generation = _generation;
267
293
  __weak __typeof__(self) weakSelf = self;
268
294
  dispatch_async(dispatch_get_main_queue(), ^{
@@ -274,13 +300,19 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
274
300
 
275
301
  - (void)clampContentOffsetIfNeeded
276
302
  {
303
+ [self offsetDiag:@"clamp-enter-or-deferred"];
277
304
  if (!_needsContentClamp || _scrollView.dragging || _scrollView.decelerating ||
278
305
  _refreshControl.refreshing || _scrollView.bounds.size.height <= 0) return;
279
306
  _needsContentClamp = NO;
280
307
  CGFloat maximum = MAX(-_scrollView.adjustedContentInset.top,
281
308
  _scrollView.contentSize.height - _scrollView.bounds.size.height + _scrollView.adjustedContentInset.bottom);
309
+ #if DEBUG
310
+ [self offsetDiag:[NSString stringWithFormat:@"clamp-candidate-max=%.4f", maximum]];
311
+ #endif
282
312
  if (_scrollView.contentOffset.y > maximum) {
313
+ [self offsetDiag:@"clamp-applied-before"];
283
314
  [_scrollView setContentOffset:CGPointMake(_scrollView.contentOffset.x, maximum) animated:NO];
315
+ [self offsetDiag:@"clamp-applied-after"];
284
316
  }
285
317
  }
286
318
 
@@ -313,12 +345,15 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
313
345
  {
314
346
  if (!_momentumActive) return;
315
347
  _momentumActive = NO;
348
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"OneKeyNativeListScrollCommandAccepted"
349
+ object:_scrollView userInfo:@{@"phase": @2}];
316
350
  [self emitScrollEvent:4 velocity:CGPointZero target:_scrollView.contentOffset];
317
351
  }
318
352
 
319
353
  - (void)stopScrolling
320
354
  {
321
355
  if (_stopping) return;
356
+ [self offsetDiag:@"stop-before"];
322
357
  _stopping = YES;
323
358
  [_scrollView setContentOffset:_scrollView.contentOffset animated:NO];
324
359
  if (_dragActive) {
@@ -330,6 +365,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
330
365
  }
331
366
  [self finishMomentum];
332
367
  _stopping = NO;
368
+ [self offsetDiag:@"stop-after"];
333
369
  }
334
370
 
335
371
  - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
@@ -394,6 +430,14 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
394
430
  - (void)scrollTo:(double)x y:(double)y animated:(BOOL)animated
395
431
  {
396
432
  if (_recycled || !isfinite(x) || !isfinite(y)) return;
433
+ for (UIView *owner = self.superview; owner != nil; owner = owner.superview) {
434
+ if ([owner conformsToProtocol:@protocol(RNCNativeScrollerInsetCoordinator)]) {
435
+ [(id<RNCNativeScrollerInsetCoordinator>)owner nativeScrollerWillReceiveScrollCommand:self];
436
+ }
437
+ }
438
+ #if DEBUG
439
+ [self offsetDiag:[NSString stringWithFormat:@"command-scrollTo-enter-x=%.4f-y=%.4f-animated=%d", x, y, animated]];
440
+ #endif
397
441
  [self stopScrolling];
398
442
  UIEdgeInsets inset = _scrollView.adjustedContentInset;
399
443
  CGPoint target = CGPointMake(MAX(-inset.left, MIN(x,
@@ -401,11 +445,15 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
401
445
  MAX(-inset.top, MIN(y,
402
446
  MAX(-inset.top, _scrollView.contentSize.height - _scrollView.bounds.size.height + inset.bottom))));
403
447
  if (CGPointEqualToPoint(target, _scrollView.contentOffset)) return;
448
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"OneKeyNativeListScrollCommandAccepted"
449
+ object:_scrollView userInfo:@{@"phase": @1}];
404
450
  if (animated) {
405
451
  _momentumActive = YES;
406
452
  [self emitScrollEvent:3 velocity:CGPointZero target:target];
407
453
  }
408
454
  [_scrollView setContentOffset:target animated:animated];
455
+ if (!animated) [[NSNotificationCenter defaultCenter] postNotificationName:@"OneKeyNativeListScrollCommandAccepted"
456
+ object:_scrollView userInfo:@{@"phase": @2}];
409
457
  }
410
458
 
411
459
  - (void)scrollToEnd:(BOOL)animated
@@ -6,11 +6,26 @@
6
6
  using namespace facebook::react;
7
7
 
8
8
  @interface RNCCollapsiblePagerViewComponentView (FirstFrameTesting)
9
+ - (void)mountingTransactionWillMount:(const MountingTransaction &)transaction
10
+ withSurfaceTelemetry:(const SurfaceTelemetry &)telemetry;
9
11
  - (void)mountingTransactionDidMount:(const MountingTransaction &)transaction
10
12
  withSurfaceTelemetry:(const SurfaceTelemetry &)telemetry;
11
13
  - (UIScrollView *)findExplicitNativeScrollerInView:(UIView *)view;
12
14
  - (UIScrollView *)findVerticalScrollViewInView:(UIView *)view;
13
15
  - (void)attachScrollObserverForCurrentPage;
16
+ - (void)attachScrollObserverForCurrentPageAfterTransition:(BOOL)selectionSettled;
17
+ - (void)detachScrollObserver;
18
+ - (void)beginPageStateTransition;
19
+ - (void)updatePageStateTransitionProgress:(CGFloat)progress;
20
+ - (void)finishPageStateTransition;
21
+ - (void)scrollToTop:(NSInteger)target animated:(BOOL)animated;
22
+ - (void)preparePageForHorizontalTransitionAtIndex:(NSInteger)index;
23
+ - (void)applyInsetsToScrollView:(UIScrollView *)scrollView pageIndex:(NSInteger)index restore:(BOOL)restore;
24
+ @end
25
+
26
+ @interface RNCNativeScrollerComponentView (ScrollCommandTesting)
27
+ - (void)scrollTo:(double)x y:(double)y animated:(BOOL)animated;
28
+ - (void)scrollToEnd:(BOOL)animated;
14
29
  @end
15
30
 
16
31
  @interface RNCCollapsiblePagerFirstFrameTests : XCTestCase
@@ -18,6 +33,284 @@ using namespace facebook::react;
18
33
 
19
34
  @implementation RNCCollapsiblePagerFirstFrameTests
20
35
 
36
+ - (void)testReachableTopCommandBeforeLayoutMustNotReplayPendingPagePosition
37
+ {
38
+ for (NSNumber *command in @[@0, @1, @2]) {
39
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
40
+ RNCNativeScrollerComponentView *scroller = [self addScrollerToPager:pager atIndex:0];
41
+ NSMutableDictionary *offsets = [pager valueForKey:@"pageOffsets"];
42
+ offsets[@"page-0"] = @660;
43
+ [pager setValue:@120 forKey:@"headerOffset"];
44
+ scroller.scrollView.contentSize = CGSizeMake(0, 0);
45
+ [pager attachScrollObserverForCurrentPage];
46
+ XCTAssertTrue([[pager valueForKey:@"pendingPageRestoreKeys"] containsObject:@"page-0"]);
47
+ XCTAssertEqualObjects(offsets[@"page-0"], @660);
48
+ XCTAssertEqual([pager valueForKey:@"observedScrollView"], scroller.scrollView);
49
+ XCTAssertEqualObjects([pager valueForKey:@"observedPageKey"], @"page-0");
50
+ XCTAssertEqualObjects([pager valueForKey:@"currentIndex"], @0);
51
+ if (command.boolValue) {
52
+ if (command.integerValue == 2) {
53
+ CGFloat reachableEndQ = MAX(-scroller.scrollView.adjustedContentInset.top,
54
+ scroller.scrollView.contentSize.height - scroller.scrollView.bounds.size.height +
55
+ scroller.scrollView.adjustedContentInset.bottom) + scroller.scrollView.contentInset.top;
56
+ XCTAssertEqualWithAccuracy(reachableEndQ, 120, 0.01);
57
+ XCTAssertEqualWithAccuracy(scroller.scrollView.contentOffset.y + scroller.scrollView.contentInset.top, reachableEndQ, 0.01);
58
+ [scroller scrollToEnd:NO];
59
+ }
60
+ else [scroller scrollTo:0 y:-scroller.scrollView.adjustedContentInset.top animated:NO];
61
+ XCTAssertEqualWithAccuracy(scroller.scrollView.contentOffset.y + scroller.scrollView.contentInset.top, command.integerValue == 2 ? 120 : 0, 0.01);
62
+ XCTAssertFalse([[pager valueForKey:@"pendingPageRestoreKeys"] containsObject:@"page-0"]);
63
+ XCTAssertEqualWithAccuracy([offsets[@"page-0"] doubleValue], 0, 0.01);
64
+ XCTAssertEqual([pager valueForKey:@"observedScrollView"], scroller.scrollView);
65
+ XCTAssertEqualObjects([pager valueForKey:@"observedPageKey"], @"page-0");
66
+ }
67
+ // The existing observed content-size path retries the pending restoration.
68
+ scroller.scrollView.contentSize = CGSizeMake(320, 1500);
69
+ XCTAssertFalse([[pager valueForKey:@"pendingPageRestoreKeys"] containsObject:@"page-0"]);
70
+ CGFloat expected = command.integerValue == 2 ? 120 : (command.boolValue ? 0 : 780);
71
+ XCTAssertEqualWithAccuracy(scroller.scrollView.contentOffset.y + scroller.scrollView.contentInset.top,
72
+ expected, 0.01, @"command=%@ must not replay a cancelled reading position", command);
73
+ XCTAssertEqualWithAccuracy([[pager valueForKey:@"headerOffset"] doubleValue], command.integerValue == 1 ? 0 : 120, 0.01);
74
+ }
75
+ }
76
+
77
+ - (void)testScrollCommandCancellationRespectsOwnerAndValidity
78
+ {
79
+ for (NSNumber *mode in @[@0, @1, @2]) {
80
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:2];
81
+ RNCNativeScrollerComponentView *current = [self addScrollerToPager:pager atIndex:0];
82
+ RNCNativeScrollerComponentView *other = [self addScrollerToPager:pager atIndex:1];
83
+ NSMutableDictionary *offsets = [pager valueForKey:@"pageOffsets"];
84
+ offsets[@"page-0"] = @660;
85
+ [pager setValue:@120 forKey:@"headerOffset"];
86
+ offsets[@"page-1"] = @360;
87
+ current.scrollView.contentSize = CGSizeMake(0, 0);
88
+ [pager attachScrollObserverForCurrentPage];
89
+ XCTAssertTrue([[pager valueForKey:@"pendingPageRestoreKeys"] containsObject:@"page-0"]);
90
+ if (mode.integerValue == 0) {
91
+ [current scrollTo:0 y:NAN animated:NO];
92
+ } else if (mode.integerValue == 1) {
93
+ [other scrollTo:0 y:-other.scrollView.adjustedContentInset.top animated:NO];
94
+ } else {
95
+ NSUInteger generation = [[pager valueForKey:@"generation"] unsignedIntegerValue];
96
+ [pager setValue:@(generation + 1) forKey:@"observedPageGeneration"];
97
+ [current scrollTo:0 y:-current.scrollView.adjustedContentInset.top animated:NO];
98
+ }
99
+ XCTAssertTrue([[pager valueForKey:@"pendingPageRestoreKeys"] containsObject:@"page-0"]);
100
+ XCTAssertEqualObjects(offsets[@"page-0"], @660);
101
+ XCTAssertEqualObjects(offsets[@"page-1"], @360);
102
+ }
103
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:2];
104
+ RNCNativeScrollerComponentView *current = [self addScrollerToPager:pager atIndex:0];
105
+ [self addScrollerToPager:pager atIndex:1];
106
+ NSMutableDictionary *offsets = [pager valueForKey:@"pageOffsets"];
107
+ offsets[@"page-0"] = @0;
108
+ offsets[@"page-1"] = @360;
109
+ [pager setValue:@120 forKey:@"headerOffset"];
110
+ [pager attachScrollObserverForCurrentPage];
111
+ XCTAssertFalse([[pager valueForKey:@"pendingPageRestoreKeys"] containsObject:@"page-0"]);
112
+ [current scrollTo:0 y:200 - current.scrollView.contentInset.top animated:NO];
113
+ XCTAssertEqualWithAccuracy(current.scrollView.contentOffset.y + current.scrollView.contentInset.top, 200, 0.01);
114
+ XCTAssertEqualWithAccuracy([offsets[@"page-0"] doubleValue], 80, 0.01);
115
+ XCTAssertEqualObjects(offsets[@"page-1"], @360);
116
+ }
117
+
118
+ - (void)testPagerTopCommandsClearOnlyCurrentReadingMemoryInBothModes
119
+ {
120
+ for (NSNumber *smooth in @[@NO, @YES]) {
121
+ for (NSNumber *target in @[@0, @1]) {
122
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:2];
123
+ [pager setValue:smooth forKey:@"nativeSmoothHeaderScrollEnabled"];
124
+ RNCNativeScrollerComponentView *current = [self addScrollerToPager:pager atIndex:0];
125
+ [self addScrollerToPager:pager atIndex:1];
126
+ NSMutableDictionary *offsets = [pager valueForKey:@"pageOffsets"];
127
+ offsets[@"page-0"] = smooth.boolValue ? @660 : @780;
128
+ offsets[@"page-1"] = @360;
129
+ [pager setValue:@120 forKey:@"headerOffset"];
130
+ [pager attachScrollObserverForCurrentPage];
131
+ [pager scrollToTop:target.integerValue animated:NO];
132
+ CGFloat expectedQ = target.boolValue ? 120 : 0;
133
+ XCTAssertEqualWithAccuracy(current.scrollView.contentOffset.y + current.scrollView.contentInset.top, expectedQ, 0.01);
134
+ XCTAssertEqualWithAccuracy([[pager valueForKey:@"headerOffset"] doubleValue], expectedQ, 0.01);
135
+ XCTAssertEqualWithAccuracy([offsets[@"page-0"] doubleValue], smooth.boolValue ? 0 : expectedQ, 0.01);
136
+ XCTAssertEqualObjects(offsets[@"page-1"], @360);
137
+ XCTAssertFalse([[pager valueForKey:@"pendingPageRestoreKeys"] containsObject:@"page-0"]);
138
+ XCTAssertNil([pager valueForKey:@"pagerTopScrollView"]);
139
+ // A same-target call cannot resurrect the old reading position.
140
+ [pager scrollToTop:target.integerValue animated:NO];
141
+ XCTAssertEqualWithAccuracy(current.scrollView.contentOffset.y + current.scrollView.contentInset.top, expectedQ, 0.01);
142
+ XCTAssertEqualObjects(offsets[@"page-1"], @360);
143
+ }
144
+ }
145
+ }
146
+
147
+ - (void)testTransitionLayoutCannotSaveSourceOffsetIntoDestinationCache
148
+ {
149
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:2];
150
+ RNCNativeScrollerComponentView *source = [self addScrollerToPager:pager atIndex:0];
151
+ RNCNativeScrollerComponentView *target = [self addScrollerToPager:pager atIndex:1];
152
+ NSMutableDictionary *offsets = [pager valueForKey:@"pageOffsets"];
153
+ offsets[@"page-0"] = @0;
154
+ offsets[@"page-1"] = @660;
155
+ [pager setValue:@120 forKey:@"headerOffset"];
156
+ [pager attachScrollObserverForCurrentPage];
157
+ XCTAssertEqualWithAccuracy(source.scrollView.contentOffset.y + source.scrollView.contentInset.top, 120, 0.01);
158
+ XCTAssertEqualObjects([pager valueForKey:@"headerOffset"], @120);
159
+ XCTAssertEqualObjects(offsets[@"page-1"], @660);
160
+ [pager detachScrollObserver];
161
+ [pager setValue:@YES forKey:@"transitioning"];
162
+ // A layout pass runs between programmatic transition start and completion.
163
+ [pager attachScrollObserverForCurrentPage];
164
+ [pager setValue:@1 forKey:@"currentIndex"];
165
+ [pager attachScrollObserverForCurrentPageAfterTransition:YES];
166
+ XCTAssertEqualWithAccuracy([offsets[@"page-1"] doubleValue], 660, 0.01);
167
+ XCTAssertEqualWithAccuracy(target.scrollView.contentOffset.y + target.scrollView.contentInset.top, 780, 0.01);
168
+ XCTAssertNotEqual([pager valueForKey:@"observedScrollView"], source.scrollView);
169
+ }
170
+
171
+ - (void)testSmoothTransitionKeepsGlobalHeaderAndIndependentBodyMemory
172
+ {
173
+ for (NSNumber *collapse in @[@0, @60, @120]) {
174
+ for (NSNumber *body in @[@0, @60, @660]) {
175
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:2];
176
+ RNCNativeScrollerComponentView *source = [self addScrollerToPager:pager atIndex:0];
177
+ RNCNativeScrollerComponentView *target = [self addScrollerToPager:pager atIndex:1];
178
+ NSMutableDictionary *offsets = [pager valueForKey:@"pageOffsets"];
179
+ offsets[@"page-0"] = @0;
180
+ offsets[@"page-1"] = body;
181
+ [pager setValue:collapse forKey:@"headerOffset"];
182
+ [pager attachScrollObserverForCurrentPage];
183
+ [pager beginPageStateTransition];
184
+ [pager setValue:@YES forKey:@"isPagerDragging"];
185
+ [pager setValue:@YES forKey:@"transitioning"];
186
+ [pager updatePageStateTransitionProgress:0.5];
187
+ CGFloat targetQ = collapse.doubleValue + (collapse.doubleValue == 120 ? body.doubleValue : 0);
188
+ XCTAssertEqualWithAccuracy([[pager valueForKey:@"headerOffset"] doubleValue], collapse.doubleValue, 0.01);
189
+ XCTAssertEqualWithAccuracy(source.scrollView.contentOffset.y + source.scrollView.contentInset.top, collapse.doubleValue, 0.01);
190
+ XCTAssertEqualWithAccuracy(target.scrollView.contentOffset.y + target.scrollView.contentInset.top, targetQ, 0.01);
191
+ XCTAssertEqualObjects(offsets[@"page-0"], @0);
192
+ XCTAssertEqualObjects(offsets[@"page-1"], body);
193
+ // Cancellation restores the source display without consuming either reading memory.
194
+ [pager finishPageStateTransition];
195
+ XCTAssertEqualWithAccuracy(source.scrollView.contentOffset.y + source.scrollView.contentInset.top, collapse.doubleValue, 0.01);
196
+ XCTAssertEqualObjects(offsets[@"page-1"], body);
197
+ [pager setValue:@NO forKey:@"isPagerDragging"];
198
+ [pager setValue:@NO forKey:@"transitioning"];
199
+ [pager attachScrollObserverForCurrentPage];
200
+ [pager beginPageStateTransition];
201
+ [pager setValue:@1 forKey:@"destinationIndex"];
202
+ [pager preparePageForHorizontalTransitionAtIndex:1];
203
+ [pager updatePageStateTransitionProgress:1];
204
+ XCTAssertEqualWithAccuracy(source.scrollView.contentOffset.y + source.scrollView.contentInset.top, collapse.doubleValue, 0.01);
205
+ [pager setValue:@1 forKey:@"currentIndex"];
206
+ [pager attachScrollObserverForCurrentPageAfterTransition:YES];
207
+ XCTAssertEqualWithAccuracy(target.scrollView.contentOffset.y + target.scrollView.contentInset.top, targetQ, 0.01);
208
+ XCTAssertEqualWithAccuracy([[pager valueForKey:@"headerOffset"] doubleValue], collapse.doubleValue, 0.01);
209
+ XCTAssertEqualObjects(offsets[@"page-1"], body);
210
+ }
211
+ }
212
+ }
213
+
214
+ - (void)testNonAdjacentTransitionUsesFixedEndpointsAndRestoresIntermediatePage
215
+ {
216
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:4];
217
+ for (NSInteger index = 0; index < 4; index++) [self addScrollerToPager:pager atIndex:index];
218
+ NSMutableDictionary *offsets = [pager valueForKey:@"pageOffsets"];
219
+ offsets[@"page-0"] = @0;
220
+ offsets[@"page-1"] = @60;
221
+ offsets[@"page-2"] = @360;
222
+ offsets[@"page-3"] = @660;
223
+ [pager setValue:@60 forKey:@"headerOffset"];
224
+ [pager attachScrollObserverForCurrentPage];
225
+ [pager beginPageStateTransition];
226
+ [pager setValue:@3 forKey:@"destinationIndex"];
227
+ [pager preparePageForHorizontalTransitionAtIndex:3];
228
+ [pager updatePageStateTransitionProgress:1];
229
+ XCTAssertEqualWithAccuracy([[pager valueForKey:@"headerOffset"] doubleValue], 60, 0.01);
230
+ [pager setValue:@3 forKey:@"currentIndex"];
231
+ [pager attachScrollObserverForCurrentPageAfterTransition:YES];
232
+ XCTAssertEqualObjects(offsets[@"page-1"], @60);
233
+ XCTAssertEqualObjects(offsets[@"page-2"], @360);
234
+ XCTAssertEqualObjects(offsets[@"page-3"], @660);
235
+ for (NSInteger index = 1; index < 4; index++) {
236
+ UIViewController *page = [pager valueForKey:@"pageControllers"][index];
237
+ RNCNativeScrollerComponentView *scroller = page.view.subviews.firstObject;
238
+ XCTAssertEqualWithAccuracy(scroller.scrollView.contentOffset.y + scroller.scrollView.contentInset.top,
239
+ 60, 0.01);
240
+ }
241
+ }
242
+
243
+ - (void)testZeroLayoutPreservesPendingBodyRestore
244
+ {
245
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
246
+ RNCNativeScrollerComponentView *scroller = [self addScrollerToPager:pager atIndex:0];
247
+ NSMutableDictionary *offsets = [pager valueForKey:@"pageOffsets"];
248
+ offsets[@"page-0"] = @660;
249
+ [pager setValue:@120 forKey:@"headerOffset"];
250
+ scroller.scrollView.contentSize = CGSizeMake(0, 0);
251
+ [pager attachScrollObserverForCurrentPage];
252
+ XCTAssertEqualObjects(offsets[@"page-0"], @660);
253
+ XCTAssertTrue([[pager valueForKey:@"pendingPageRestoreKeys"] containsObject:@"page-0"]);
254
+ // Releasing an observed but unmeasured identity must retain its pending B.
255
+ scroller.tag = 91001;
256
+ ShadowView removedView;
257
+ removedView.tag = (Tag)scroller.tag;
258
+ MountingTransaction removal(1, 1, {ShadowViewMutation::RemoveMutation(1, removedView, 0)}, {});
259
+ SurfaceTelemetry telemetry;
260
+ [pager mountingTransactionWillMount:removal withSurfaceTelemetry:telemetry];
261
+ XCTAssertEqualObjects(offsets[@"page-0"], @660);
262
+ [self finishMount:pager];
263
+ XCTAssertEqualObjects(offsets[@"page-0"], @660);
264
+ scroller.scrollView.contentSize = CGSizeMake(320, 1500);
265
+ XCTAssertEqualObjects(offsets[@"page-0"], @660);
266
+ XCTAssertFalse([[pager valueForKey:@"pendingPageRestoreKeys"] containsObject:@"page-0"]);
267
+ XCTAssertEqualWithAccuracy(scroller.scrollView.contentOffset.y + scroller.scrollView.contentInset.top, 780, 0.01);
268
+ }
269
+
270
+ - (void)testUnresolvedReplacementDoesNotCommitTemporaryClamp
271
+ {
272
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:2];
273
+ RNCNativeScrollerComponentView *source = [self addScrollerToPager:pager atIndex:0];
274
+ NSMutableDictionary *offsets = [pager valueForKey:@"pageOffsets"];
275
+ offsets[@"page-0"] = @0;
276
+ offsets[@"page-1"] = @660;
277
+ [pager setValue:@120 forKey:@"headerOffset"];
278
+ [pager attachScrollObserverForCurrentPage];
279
+ [pager beginPageStateTransition];
280
+ [pager setValue:@1 forKey:@"destinationIndex"];
281
+ UICollectionView *replacement = [[UICollectionView alloc]
282
+ initWithFrame:CGRectMake(0, 0, 320, 600)
283
+ collectionViewLayout:[UICollectionViewFlowLayout new]];
284
+ UIViewController *page = [pager valueForKey:@"pageControllers"][1];
285
+ [page.view addSubview:replacement];
286
+ id released = [NSClassFromString(@"RNCCollapsiblePagerReleasedScrollState") new];
287
+ XCTAssertNotNil(released);
288
+ [released setValue:source.scrollView forKey:@"scrollView"];
289
+ [released setValue:source forKey:@"componentView"];
290
+ [released setValue:@(source.tag) forKey:@"componentTag"];
291
+ [pager valueForKey:@"releasedPageScrollStates"][@"page-1"] = released;
292
+ [pager preparePageForHorizontalTransitionAtIndex:1];
293
+ [pager setValue:@1 forKey:@"currentIndex"];
294
+ [pager finishPageStateTransition];
295
+ XCTAssertEqualObjects(offsets[@"page-1"], @660);
296
+ XCTAssertTrue([[pager valueForKey:@"pendingPageRestoreKeys"] containsObject:@"page-1"]);
297
+ }
298
+
299
+ - (void)testRenamedPageDoesNotResurrectPrunedObserverCache
300
+ {
301
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
302
+ RNCNativeScrollerComponentView *scroller = [self addScrollerToPager:pager atIndex:0];
303
+ NSMutableDictionary *offsets = [pager valueForKey:@"pageOffsets"];
304
+ offsets[@"page-0"] = @60;
305
+ [pager attachScrollObserverForCurrentPage];
306
+ [pager setValue:@[@"replacement-key"] forKey:@"pageKeys"];
307
+ [offsets removeObjectForKey:@"page-0"];
308
+ scroller.scrollView.contentOffset = CGPointMake(0, 200);
309
+ [pager detachScrollObserver];
310
+ XCTAssertNil(offsets[@"page-0"]);
311
+ XCTAssertNil(offsets[@"replacement-key"]);
312
+ }
313
+
21
314
  - (RNCCollapsiblePagerViewComponentView *)pagerWithPageCount:(NSInteger)count
22
315
  {
23
316
  RNCCollapsiblePagerViewComponentView *pager =
@@ -1,6 +1,7 @@
1
1
  #import <XCTest/XCTest.h>
2
2
  #import <React/RCTViewComponentView.h>
3
3
  #import "RNCCollapsiblePagerViewComponentView.h"
4
+ #import "RNCNativeScrollerComponentView.h"
4
5
 
5
6
  @interface RNCCollapsiblePagerViewComponentView (RefreshControlTesting)
6
7
  - (void)updateRefreshControlForScrollView:(UIScrollView *)scrollView;
@@ -8,6 +9,10 @@
8
9
  - (void)attachScrollObserverForCurrentPage;
9
10
  @end
10
11
 
12
+ @interface RNCNativeScrollerComponentView (RefreshControlScrollCommandTesting)
13
+ - (void)scrollTo:(double)x y:(double)y animated:(BOOL)animated;
14
+ @end
15
+
11
16
  @interface RNCCollapsiblePagerRefreshControlTests : XCTestCase
12
17
  @end
13
18
 
@@ -184,14 +189,18 @@
184
189
  RNCCollapsiblePagerViewComponentView *pager = [self pagerWithHeader:64];
185
190
  [pager mountChildComponentView:[RCTViewComponentView new] index:0];
186
191
  [pager mountChildComponentView:[RCTViewComponentView new] index:1];
187
- UIScrollView *scroll = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 320, 600)];
192
+ RNCNativeScrollerComponentView *scroller = [RNCNativeScrollerComponentView new];
193
+ scroller.frame = CGRectMake(0, 0, 320, 600);
194
+ scroller.scrollView.frame = scroller.bounds;
195
+ UIScrollView *scroll = scroller.scrollView;
188
196
  scroll.refreshControl = [UIRefreshControl new];
189
197
  scroll.contentSize = CGSizeMake(320, 2000);
190
198
  scroll.contentInset = UIEdgeInsetsMake(232, 0, 0, 0);
191
199
  UIView *content = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 2000)];
192
200
  [scroll addSubview:content];
193
201
  UIViewController *page = [UIViewController new];
194
- [page.view addSubview:scroll];
202
+ [page.view addSubview:scroller];
203
+ [pager addSubview:page.view];
195
204
  NSMutableArray<UIViewController *> *pages = [pager valueForKey:@"pageControllers"];
196
205
  [pages addObject:page];
197
206
  [pager attachScrollObserverForCurrentPage];
@@ -204,7 +213,7 @@
204
213
  XCTAssertEqualWithAccuracy(scroll.contentInset.top, 340, 0.001);
205
214
  XCTAssertEqualWithAccuracy(headerHost.transform.ty, 0, 0.001);
206
215
 
207
- scroll.contentOffset = CGPointMake(0, 500);
216
+ [scroller scrollTo:0 y:500 animated:NO];
208
217
  CGFloat baselineStickyTranslation = stickyHeader.transform.ty;
209
218
  CGFloat baselineTabBarTranslation = tabBar.transform.ty;
210
219
  XCTAssertEqualWithAccuracy(baselineStickyTranslation, 776, 0.001);
@@ -217,11 +226,11 @@
217
226
  XCTAssertEqualWithAccuracy(tabBar.transform.ty, baselineTabBarTranslation, 0.001);
218
227
  XCTAssertEqualWithAccuracy(CGRectGetMaxY(headerHost.frame), CGRectGetMinY(content.frame), 0.001);
219
228
 
220
- // Exercise actual scroll notifications while the temporary refresh inset is
229
+ // Exercise accepted native commands while the temporary refresh inset is
221
230
  // present, including direction reversals and the sticky/unpinned boundary.
222
231
  // The pre-fix formula adds the extra 60 points on these scroll updates.
223
232
  for (NSNumber *nextOffset in @[@620, @450, @-300, @-340, @500]) {
224
- scroll.contentOffset = CGPointMake(0, nextOffset.doubleValue);
233
+ [scroller scrollTo:0 y:nextOffset.doubleValue animated:NO];
225
234
  CGFloat expectedTranslation = MAX(0, nextOffset.doubleValue + 340 - 64);
226
235
  XCTAssertEqualWithAccuracy(headerHost.transform.ty, 0, 0.001);
227
236
  XCTAssertEqualWithAccuracy(stickyHeader.transform.ty, expectedTranslation, 0.001);
@@ -71,6 +71,12 @@ export class CollapsiblePagerView extends React.PureComponent {
71
71
  }
72
72
  }
73
73
  };
74
+ scrollToPageTop = options => {
75
+ if (this.nativeRef) CollapsiblePagerViewNativeCommands.scrollToTop(this.nativeRef, 0, options?.animated ?? true);
76
+ };
77
+ scrollToStickyTop = options => {
78
+ if (this.nativeRef) CollapsiblePagerViewNativeCommands.scrollToTop(this.nativeRef, 1, options?.animated ?? true);
79
+ };
74
80
  setScrollEnabled = scrollEnabled => {
75
81
  if (this.nativeRef) {
76
82
  CollapsiblePagerViewNativeCommands.setScrollEnabledImperatively(this.nativeRef, scrollEnabled);