@onekeyfe/react-native-pager-view 3.0.111 → 3.0.113

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.
@@ -0,0 +1,738 @@
1
+ #import "RNCCollapsiblePagerViewComponentView.h"
2
+
3
+ #import <react/renderer/components/pagerview/ComponentDescriptors.h>
4
+ #import <react/renderer/components/pagerview/EventEmitters.h>
5
+ #import <react/renderer/components/pagerview/Props.h>
6
+ #import <react/renderer/components/pagerview/RCTComponentViewHelpers.h>
7
+
8
+ #import "React/RCTConversions.h"
9
+
10
+ using namespace facebook::react;
11
+
12
+ static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerContentOffsetContext;
13
+
14
+ @interface RNCCollapsiblePagerViewComponentView () <
15
+ RCTRNCCollapsiblePagerViewViewProtocol,
16
+ UIPageViewControllerDataSource,
17
+ UIPageViewControllerDelegate,
18
+ UIScrollViewDelegate
19
+ >
20
+ @end
21
+
22
+ @implementation RNCCollapsiblePagerViewComponentView {
23
+ UIView *_containerView;
24
+ UIPageViewController *_pageViewController;
25
+ UIScrollView *_pagerScrollView;
26
+ NSMutableArray<UIView<RCTComponentViewProtocol> *> *_logicalChildren;
27
+ NSMutableArray<UIViewController *> *_pageControllers;
28
+ UIView *_headerView;
29
+ UIView *_stickyHeaderView;
30
+ NSInteger _currentIndex;
31
+ NSInteger _destinationIndex;
32
+ NSInteger _pendingInitialPage;
33
+ NSInteger _headerHeight;
34
+ NSInteger _stickyHeaderHeight;
35
+ CGFloat _headerOffset;
36
+ BOOL _scrollEnabled;
37
+ BOOL _transitioning;
38
+ BOOL _hasAppliedInitialPage;
39
+ NSString *_layoutDirection;
40
+ NSArray<NSString *> *_pageKeys;
41
+ NSString *_retainedPages;
42
+ NSMutableDictionary<NSString *, NSNumber *> *_pageOffsets;
43
+ NSMapTable<UIScrollView *, NSValue *> *_originalInsets;
44
+ NSMapTable<UIScrollView *, NSValue *> *_originalIndicatorInsets;
45
+ NSMapTable<UIScrollView *, NSNumber *> *_originalAlwaysBounceVertical;
46
+ NSMapTable<UIScrollView *, NSNumber *> *_originalInsetAdjustmentBehavior;
47
+ __weak UIScrollView *_observedScrollView;
48
+ BOOL _observingContentOffset;
49
+ BOOL _isBeingRecycled;
50
+ NSUInteger _generation;
51
+ }
52
+
53
+ + (void)load
54
+ {
55
+ [super load];
56
+ }
57
+
58
+ - (instancetype)initWithFrame:(CGRect)frame
59
+ {
60
+ if (self = [super initWithFrame:frame]) {
61
+ static const auto defaultProps = std::make_shared<const RNCCollapsiblePagerViewProps>();
62
+ _props = defaultProps;
63
+ _logicalChildren = [NSMutableArray new];
64
+ _pageControllers = [NSMutableArray new];
65
+ _pageOffsets = [NSMutableDictionary new];
66
+ _originalInsets = [NSMapTable weakToStrongObjectsMapTable];
67
+ _originalIndicatorInsets = [NSMapTable weakToStrongObjectsMapTable];
68
+ _originalAlwaysBounceVertical = [NSMapTable weakToStrongObjectsMapTable];
69
+ _originalInsetAdjustmentBehavior = [NSMapTable weakToStrongObjectsMapTable];
70
+ _pageKeys = @[];
71
+ _retainedPages = @"[]";
72
+ _currentIndex = 0;
73
+ _destinationIndex = 0;
74
+ _pendingInitialPage = 0;
75
+ _scrollEnabled = YES;
76
+ _hasAppliedInitialPage = NO;
77
+ _layoutDirection = @"ltr";
78
+
79
+ _containerView = [UIView new];
80
+ _containerView.clipsToBounds = YES;
81
+ self.contentView = _containerView;
82
+
83
+ [self initializePageViewController];
84
+ }
85
+ return self;
86
+ }
87
+
88
+ - (void)willMoveToSuperview:(UIView *)newSuperview
89
+ {
90
+ [super willMoveToSuperview:newSuperview];
91
+ if (newSuperview != nil && _pageViewController == nil) {
92
+ self.contentView = _containerView;
93
+ [self initializePageViewController];
94
+ }
95
+ }
96
+
97
+ - (void)initializePageViewController
98
+ {
99
+ _pageViewController = [[UIPageViewController alloc]
100
+ initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
101
+ navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
102
+ options:nil];
103
+ _pageViewController.dataSource = self;
104
+ _pageViewController.delegate = self;
105
+ [_containerView addSubview:_pageViewController.view];
106
+
107
+ for (UIView *subview in _pageViewController.view.subviews) {
108
+ if ([subview isKindOfClass:UIScrollView.class]) {
109
+ _pagerScrollView = (UIScrollView *)subview;
110
+ _pagerScrollView.delegate = self;
111
+ _pagerScrollView.delaysContentTouches = NO;
112
+ _pagerScrollView.scrollEnabled = _scrollEnabled;
113
+ break;
114
+ }
115
+ }
116
+ }
117
+
118
+ #pragma mark - React mounting
119
+
120
+ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
121
+ index:(NSInteger)index
122
+ {
123
+ if (_isBeingRecycled) return;
124
+ NSInteger safeIndex = MIN(MAX(index, 0), _logicalChildren.count);
125
+ [_logicalChildren insertObject:childComponentView atIndex:safeIndex];
126
+ [self rebuildSlots];
127
+ }
128
+
129
+ - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
130
+ index:(NSInteger)index
131
+ {
132
+ if (_isBeingRecycled) return;
133
+ NSUInteger existingIndex = [_logicalChildren indexOfObjectIdenticalTo:childComponentView];
134
+ if (existingIndex == NSNotFound) return;
135
+ [childComponentView removeFromSuperview];
136
+ [_logicalChildren removeObjectAtIndex:existingIndex];
137
+ [self rebuildSlots];
138
+ }
139
+
140
+ - (void)rebuildSlots
141
+ {
142
+ [self detachScrollObserver];
143
+ [_headerView removeFromSuperview];
144
+ [_stickyHeaderView removeFromSuperview];
145
+ _headerView = nil;
146
+ _stickyHeaderView = nil;
147
+ [_pageControllers removeAllObjects];
148
+
149
+ if (_logicalChildren.count > 0) {
150
+ _headerView = _logicalChildren[0];
151
+ [_containerView addSubview:_headerView];
152
+ }
153
+ if (_logicalChildren.count > 1) {
154
+ _stickyHeaderView = _logicalChildren[1];
155
+ [_containerView addSubview:_stickyHeaderView];
156
+ }
157
+ for (NSInteger index = 2; index < _logicalChildren.count; index++) {
158
+ UIView *page = _logicalChildren[index];
159
+ UIViewController *controller = [UIViewController new];
160
+ [controller.view addSubview:page];
161
+ [_pageControllers addObject:controller];
162
+ }
163
+
164
+ if (_pageControllers.count > 0) {
165
+ if (!_hasAppliedInitialPage &&
166
+ _pendingInitialPage >= 0 &&
167
+ _pendingInitialPage < _pageControllers.count) {
168
+ _currentIndex = _pendingInitialPage;
169
+ _hasAppliedInitialPage = YES;
170
+ } else {
171
+ _currentIndex = MIN(MAX(_currentIndex, 0), _pageControllers.count - 1);
172
+ }
173
+ UIViewController *controller = _pageControllers[_currentIndex];
174
+ [_pageViewController setViewControllers:@[controller]
175
+ direction:UIPageViewControllerNavigationDirectionForward
176
+ animated:NO
177
+ completion:nil];
178
+ }
179
+ if (_headerView != nil) [_containerView bringSubviewToFront:_headerView];
180
+ if (_stickyHeaderView != nil) [_containerView bringSubviewToFront:_stickyHeaderView];
181
+ [self setNeedsLayout];
182
+ dispatch_async(dispatch_get_main_queue(), ^{
183
+ if (!self->_isBeingRecycled) {
184
+ [self prepareAdjacentPageInsets];
185
+ [self attachScrollObserverForCurrentPage];
186
+ }
187
+ });
188
+ }
189
+
190
+ - (void)layoutSubviews
191
+ {
192
+ [super layoutSubviews];
193
+ _containerView.frame = self.bounds;
194
+ _pageViewController.view.frame = _containerView.bounds;
195
+ // UIKit frame assignment is undefined while a view has a non-identity transform.
196
+ // Lay out the original slots, then restore the shared collapse translation.
197
+ _headerView.transform = CGAffineTransformIdentity;
198
+ _stickyHeaderView.transform = CGAffineTransformIdentity;
199
+ _headerView.frame = CGRectMake(0, 0, self.bounds.size.width, _headerHeight);
200
+ _stickyHeaderView.frame = CGRectMake(
201
+ 0,
202
+ _headerHeight,
203
+ self.bounds.size.width,
204
+ _stickyHeaderHeight
205
+ );
206
+ for (UIViewController *controller in _pageControllers) {
207
+ controller.view.frame = _pageViewController.view.bounds;
208
+ controller.view.subviews.firstObject.frame = controller.view.bounds;
209
+ }
210
+ [self applyHeaderOffset];
211
+ dispatch_async(dispatch_get_main_queue(), ^{
212
+ if (!self->_isBeingRecycled) {
213
+ [self prepareAdjacentPageInsets];
214
+ [self attachScrollObserverForCurrentPage];
215
+ }
216
+ });
217
+ }
218
+
219
+ - (void)prepareForRecycle
220
+ {
221
+ _isBeingRecycled = YES;
222
+ _generation++;
223
+ [self detachScrollObserver];
224
+ for (UIScrollView *scrollView in _originalInsets.keyEnumerator) {
225
+ NSValue *value = [_originalInsets objectForKey:scrollView];
226
+ if (value != nil) scrollView.contentInset = value.UIEdgeInsetsValue;
227
+ NSValue *indicatorValue = [_originalIndicatorInsets objectForKey:scrollView];
228
+ if (indicatorValue != nil) {
229
+ scrollView.verticalScrollIndicatorInsets = indicatorValue.UIEdgeInsetsValue;
230
+ }
231
+ NSNumber *alwaysBounce = [_originalAlwaysBounceVertical objectForKey:scrollView];
232
+ if (alwaysBounce != nil) scrollView.alwaysBounceVertical = alwaysBounce.boolValue;
233
+ NSNumber *adjustment = [_originalInsetAdjustmentBehavior objectForKey:scrollView];
234
+ if (adjustment != nil) {
235
+ scrollView.contentInsetAdjustmentBehavior = (UIScrollViewContentInsetAdjustmentBehavior)adjustment.integerValue;
236
+ }
237
+ }
238
+ [_originalInsets removeAllObjects];
239
+ [_originalIndicatorInsets removeAllObjects];
240
+ [_originalAlwaysBounceVertical removeAllObjects];
241
+ [_originalInsetAdjustmentBehavior removeAllObjects];
242
+ [_headerView removeFromSuperview];
243
+ [_stickyHeaderView removeFromSuperview];
244
+ [_logicalChildren removeAllObjects];
245
+ [_pageControllers removeAllObjects];
246
+ _headerView = nil;
247
+ _stickyHeaderView = nil;
248
+ _pageViewController.dataSource = nil;
249
+ _pageViewController.delegate = nil;
250
+ _pagerScrollView.delegate = nil;
251
+ [_pageViewController.view removeFromSuperview];
252
+ _pageViewController = nil;
253
+ _pagerScrollView = nil;
254
+ [super prepareForRecycle];
255
+ [_pageOffsets removeAllObjects];
256
+ _pageKeys = @[];
257
+ _retainedPages = @"[]";
258
+ _currentIndex = 0;
259
+ _destinationIndex = 0;
260
+ _pendingInitialPage = 0;
261
+ _headerHeight = 0;
262
+ _stickyHeaderHeight = 0;
263
+ _headerOffset = 0;
264
+ _scrollEnabled = YES;
265
+ _transitioning = NO;
266
+ _hasAppliedInitialPage = NO;
267
+ _layoutDirection = @"ltr";
268
+ _isBeingRecycled = NO;
269
+ }
270
+
271
+ #pragma mark - Props
272
+
273
+ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
274
+ {
275
+ const auto &oldViewProps = *std::static_pointer_cast<const RNCCollapsiblePagerViewProps>(_props);
276
+ const auto &newViewProps = *std::static_pointer_cast<const RNCCollapsiblePagerViewProps>(props);
277
+
278
+ if (oldViewProps.initialPage != newViewProps.initialPage) {
279
+ _pendingInitialPage = MAX(0, newViewProps.initialPage);
280
+ _hasAppliedInitialPage = NO;
281
+ }
282
+ if (oldViewProps.scrollEnabled != newViewProps.scrollEnabled) {
283
+ _scrollEnabled = newViewProps.scrollEnabled;
284
+ _pagerScrollView.scrollEnabled = _scrollEnabled;
285
+ }
286
+ if (oldViewProps.layoutDirection != newViewProps.layoutDirection) {
287
+ _layoutDirection = RCTNSStringFromString(toString(newViewProps.layoutDirection));
288
+ }
289
+ BOOL insetsChanged = NO;
290
+ if (oldViewProps.headerHeight != newViewProps.headerHeight) {
291
+ _headerHeight = MAX(0, newViewProps.headerHeight);
292
+ _headerOffset = MIN(_headerOffset, _headerHeight);
293
+ insetsChanged = YES;
294
+ }
295
+ if (oldViewProps.stickyHeaderHeight != newViewProps.stickyHeaderHeight) {
296
+ _stickyHeaderHeight = MAX(0, newViewProps.stickyHeaderHeight);
297
+ insetsChanged = YES;
298
+ }
299
+ if (oldViewProps.pageKeys != newViewProps.pageKeys) {
300
+ NSString *json = RCTNSStringFromString(newViewProps.pageKeys);
301
+ NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding];
302
+ id parsed = data == nil ? nil : [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
303
+ _pageKeys = [parsed isKindOfClass:NSArray.class] ? parsed : @[];
304
+ }
305
+ if (oldViewProps.retainedPages != newViewProps.retainedPages) {
306
+ _retainedPages = RCTNSStringFromString(newViewProps.retainedPages);
307
+ [self setNeedsLayout];
308
+ }
309
+
310
+ [super updateProps:props oldProps:oldProps];
311
+ if (insetsChanged) {
312
+ [self reapplyInsetsToObservedScrollView];
313
+ [self setNeedsLayout];
314
+ }
315
+ }
316
+
317
+ #pragma mark - Collapsible scroll coordination
318
+
319
+ - (NSString *)currentPageKey
320
+ {
321
+ return [self pageKeyForIndex:_currentIndex];
322
+ }
323
+
324
+ - (NSString *)pageKeyForIndex:(NSInteger)index
325
+ {
326
+ if (index >= 0 && index < _pageKeys.count) return _pageKeys[index];
327
+ return [NSString stringWithFormat:@"page-%ld", (long)index];
328
+ }
329
+
330
+ - (UIScrollView *)findVerticalScrollViewInView:(UIView *)view
331
+ {
332
+ if ([view isKindOfClass:UICollectionView.class]) {
333
+ UICollectionView *collectionView = (UICollectionView *)view;
334
+ UICollectionViewLayout *layout = collectionView.collectionViewLayout;
335
+ if (![layout isKindOfClass:UICollectionViewFlowLayout.class] ||
336
+ ((UICollectionViewFlowLayout *)layout).scrollDirection == UICollectionViewScrollDirectionVertical) {
337
+ return collectionView;
338
+ }
339
+ }
340
+ // Empty pages may use an RN ScrollView instead of a native row list.
341
+ if ([view isKindOfClass:UIScrollView.class] &&
342
+ ![view isKindOfClass:UICollectionView.class]) {
343
+ UIScrollView *scrollView = (UIScrollView *)view;
344
+ if (!scrollView.alwaysBounceHorizontal &&
345
+ scrollView.contentSize.width <= scrollView.bounds.size.width + 1) {
346
+ return scrollView;
347
+ }
348
+ }
349
+ for (UIView *subview in view.subviews) {
350
+ UIScrollView *candidate = [self findVerticalScrollViewInView:subview];
351
+ if (candidate != nil) return candidate;
352
+ }
353
+ return nil;
354
+ }
355
+
356
+ - (void)attachScrollObserverForCurrentPage
357
+ {
358
+ [self restoreDetachedScrollInsets];
359
+ if (_currentIndex < 0 || _currentIndex >= _pageControllers.count) return;
360
+ UIScrollView *candidate = [self findVerticalScrollViewInView:_pageControllers[_currentIndex].view];
361
+ if (candidate == nil || candidate == _observedScrollView) return;
362
+
363
+ BOOL replacingCurrentScrollView = _observingContentOffset;
364
+ [self detachScrollObserver];
365
+ if (replacingCurrentScrollView) {
366
+ // A list replaced by an empty RN view keeps collapse, not the old row offset.
367
+ _pageOffsets[self.currentPageKey] = @(_headerOffset);
368
+ }
369
+ _observedScrollView = candidate;
370
+ [self applyInsetsToScrollView:candidate pageIndex:_currentIndex restore:YES];
371
+ [candidate addObserver:self
372
+ forKeyPath:@"contentOffset"
373
+ options:NSKeyValueObservingOptionNew
374
+ context:RNCCollapsiblePagerContentOffsetContext];
375
+ [candidate addObserver:self
376
+ forKeyPath:@"contentSize"
377
+ options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
378
+ context:RNCCollapsiblePagerContentOffsetContext];
379
+ _observingContentOffset = YES;
380
+ }
381
+
382
+ - (void)detachScrollObserver
383
+ {
384
+ UIScrollView *scrollView = _observedScrollView;
385
+ if (scrollView != nil) {
386
+ _pageOffsets[self.currentPageKey] = @(scrollView.contentOffset.y + scrollView.contentInset.top);
387
+ if (_observingContentOffset) {
388
+ [scrollView removeObserver:self
389
+ forKeyPath:@"contentOffset"
390
+ context:RNCCollapsiblePagerContentOffsetContext];
391
+ [scrollView removeObserver:self
392
+ forKeyPath:@"contentSize"
393
+ context:RNCCollapsiblePagerContentOffsetContext];
394
+ }
395
+ }
396
+ _observingContentOffset = NO;
397
+ _observedScrollView = nil;
398
+ }
399
+
400
+ - (void)restoreDetachedScrollInsets
401
+ {
402
+ for (UIScrollView *scrollView in _originalInsets.keyEnumerator.allObjects) {
403
+ BOOL belongsToPage = NO;
404
+ for (UIViewController *controller in _pageControllers) {
405
+ if ([scrollView isDescendantOfView:controller.view]) {
406
+ belongsToPage = YES;
407
+ break;
408
+ }
409
+ }
410
+ if (belongsToPage) continue;
411
+ if (scrollView == _observedScrollView) {
412
+ [self detachScrollObserver];
413
+ _pageOffsets[self.currentPageKey] = @(_headerOffset);
414
+ }
415
+ // Fabric can reuse an empty RN ScrollView in another screen. Release all
416
+ // pager-owned insets before that view is used as a search or dialog list.
417
+ CGFloat logicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
418
+ UIEdgeInsets original = [_originalInsets objectForKey:scrollView].UIEdgeInsetsValue;
419
+ scrollView.contentInset = original;
420
+ scrollView.verticalScrollIndicatorInsets =
421
+ [_originalIndicatorInsets objectForKey:scrollView].UIEdgeInsetsValue;
422
+ scrollView.alwaysBounceVertical =
423
+ [_originalAlwaysBounceVertical objectForKey:scrollView].boolValue;
424
+ scrollView.contentInsetAdjustmentBehavior = (UIScrollViewContentInsetAdjustmentBehavior)
425
+ [_originalInsetAdjustmentBehavior objectForKey:scrollView].integerValue;
426
+ [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, logicalOffset - original.top)
427
+ animated:NO];
428
+ [_originalInsets removeObjectForKey:scrollView];
429
+ [_originalIndicatorInsets removeObjectForKey:scrollView];
430
+ [_originalAlwaysBounceVertical removeObjectForKey:scrollView];
431
+ [_originalInsetAdjustmentBehavior removeObjectForKey:scrollView];
432
+ }
433
+ }
434
+
435
+ - (void)applyInsetsToScrollView:(UIScrollView *)scrollView
436
+ pageIndex:(NSInteger)pageIndex
437
+ restore:(BOOL)restore
438
+ {
439
+ NSValue *storedOriginal = [_originalInsets objectForKey:scrollView];
440
+ UIEdgeInsets original = storedOriginal == nil
441
+ ? scrollView.contentInset
442
+ : storedOriginal.UIEdgeInsetsValue;
443
+ if (storedOriginal == nil) {
444
+ [_originalInsets setObject:[NSValue valueWithUIEdgeInsets:original] forKey:scrollView];
445
+ [_originalIndicatorInsets
446
+ setObject:[NSValue valueWithUIEdgeInsets:scrollView.verticalScrollIndicatorInsets]
447
+ forKey:scrollView];
448
+ [_originalAlwaysBounceVertical setObject:@(scrollView.alwaysBounceVertical) forKey:scrollView];
449
+ [_originalInsetAdjustmentBehavior setObject:@(scrollView.contentInsetAdjustmentBehavior) forKey:scrollView];
450
+ }
451
+
452
+ // This host already includes the fixed header and safe area in its frame.
453
+ // UIKit automatic adjustment would add them a second time to short pages.
454
+ scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
455
+ CGFloat previousLogicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
456
+ UIEdgeInsets next = original;
457
+ next.top += _headerHeight + _stickyHeaderHeight;
458
+ // Short pages still need enough range to collapse the header; long pages
459
+ // must end at the last row rather than leave a header-sized blank footer.
460
+ next.bottom += MAX(0, scrollView.bounds.size.height - _stickyHeaderHeight
461
+ - original.top - original.bottom - scrollView.contentSize.height);
462
+ if (UIEdgeInsetsEqualToEdgeInsets(scrollView.contentInset, next) && !restore) {
463
+ // UICollectionView may report contentSize during a bounce without changing
464
+ // the required insets. Re-setting contentOffset here interrupts that bounce.
465
+ if (pageIndex == _currentIndex) [self updateHeaderForScrollView:scrollView];
466
+ return;
467
+ }
468
+ scrollView.contentInset = next;
469
+ UIEdgeInsets indicatorInsets = scrollView.verticalScrollIndicatorInsets;
470
+ indicatorInsets.top = next.top;
471
+ scrollView.verticalScrollIndicatorInsets = indicatorInsets;
472
+ scrollView.alwaysBounceVertical = YES;
473
+
474
+ CGFloat targetOffset = previousLogicalOffset - next.top;
475
+ if (restore) {
476
+ NSNumber *saved = _pageOffsets[[self pageKeyForIndex:pageIndex]];
477
+ // The sticky slot can change height between pages. Cache offsets relative
478
+ // to content start so a round trip does not add that height difference.
479
+ targetOffset = (saved == nil ? _headerOffset : saved.doubleValue) - next.top;
480
+ targetOffset = MAX(targetOffset, -next.top + _headerOffset);
481
+ }
482
+ [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, targetOffset) animated:NO];
483
+ if (pageIndex == _currentIndex) [self updateHeaderForScrollView:scrollView];
484
+ }
485
+
486
+ - (void)reapplyInsetsToObservedScrollView
487
+ {
488
+ [self restoreDetachedScrollInsets];
489
+ UIScrollView *scrollView = _observedScrollView;
490
+ if (scrollView != nil) {
491
+ [self applyInsetsToScrollView:scrollView pageIndex:_currentIndex restore:NO];
492
+ }
493
+ [self prepareAdjacentPageInsets];
494
+ }
495
+
496
+ - (void)prepareAdjacentPageInsets
497
+ {
498
+ [self restoreDetachedScrollInsets];
499
+ NSInteger first = MAX(0, _currentIndex - 1);
500
+ NSInteger last = MIN((NSInteger)_pageControllers.count - 1, _currentIndex + 1);
501
+ if (last < first) return;
502
+ for (NSInteger index = first; index <= last; index++) {
503
+ UIScrollView *scrollView = [self findVerticalScrollViewInView:_pageControllers[index].view];
504
+ if (scrollView != nil && scrollView != _observedScrollView) {
505
+ [self applyInsetsToScrollView:scrollView pageIndex:index restore:YES];
506
+ }
507
+ }
508
+ }
509
+
510
+ - (void)observeValueForKeyPath:(NSString *)keyPath
511
+ ofObject:(id)object
512
+ change:(NSDictionary<NSKeyValueChangeKey,id> *)change
513
+ context:(void *)context
514
+ {
515
+ if (context == RNCCollapsiblePagerContentOffsetContext && object == _observedScrollView) {
516
+ if ([keyPath isEqualToString:@"contentSize"]) {
517
+ NSValue *oldSize = change[NSKeyValueChangeOldKey];
518
+ NSValue *newSize = change[NSKeyValueChangeNewKey];
519
+ if (![oldSize isEqual:newSize]) {
520
+ [self applyInsetsToScrollView:_observedScrollView pageIndex:_currentIndex restore:NO];
521
+ }
522
+ return;
523
+ }
524
+ UIScrollView *scrollView = (UIScrollView *)object;
525
+ _pageOffsets[self.currentPageKey] = @(scrollView.contentOffset.y + scrollView.contentInset.top);
526
+ [self updateHeaderForScrollView:scrollView];
527
+ return;
528
+ }
529
+ [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
530
+ }
531
+
532
+ - (void)updateHeaderForScrollView:(UIScrollView *)scrollView
533
+ {
534
+ CGFloat logicalOffset = scrollView.contentOffset.y + scrollView.contentInset.top;
535
+ _headerOffset = MIN(MAX(logicalOffset, 0), _headerHeight);
536
+ [self applyHeaderOffset];
537
+ }
538
+
539
+ - (void)applyHeaderOffset
540
+ {
541
+ CGAffineTransform transform = CGAffineTransformMakeTranslation(0, -_headerOffset);
542
+ _headerView.transform = transform;
543
+ _stickyHeaderView.transform = transform;
544
+ }
545
+
546
+ #pragma mark - Pager navigation
547
+
548
+ - (BOOL)isLtrLayout
549
+ {
550
+ return [_layoutDirection isEqualToString:@"ltr"];
551
+ }
552
+
553
+ - (void)goTo:(NSInteger)index animated:(BOOL)animated
554
+ {
555
+ if (_isBeingRecycled || index < 0 || index >= _pageControllers.count) return;
556
+ if (_transitioning && animated) return;
557
+
558
+ [self detachScrollObserver];
559
+ _destinationIndex = index;
560
+ BOOL forward = (index > _currentIndex && self.isLtrLayout) ||
561
+ (index < _currentIndex && !self.isLtrLayout);
562
+ UIPageViewControllerNavigationDirection direction = forward
563
+ ? UIPageViewControllerNavigationDirectionForward
564
+ : UIPageViewControllerNavigationDirectionReverse;
565
+ _transitioning = YES;
566
+ NSUInteger capturedGeneration = _generation;
567
+ __weak __typeof__(self) weakSelf = self;
568
+ [_pageViewController setViewControllers:@[_pageControllers[index]]
569
+ direction:direction
570
+ animated:animated && index != _currentIndex
571
+ completion:^(BOOL finished) {
572
+ __strong __typeof__(weakSelf) self = weakSelf;
573
+ if (self == nil || self->_generation != capturedGeneration || self->_isBeingRecycled) return;
574
+ self->_transitioning = NO;
575
+ self->_currentIndex = index;
576
+ [self attachScrollObserverForCurrentPage];
577
+ [self emitPageSelected:index];
578
+ [self emitDiagnostics:@"page-selected"];
579
+ }];
580
+ }
581
+
582
+ - (UIViewController *)adjacentController:(UIViewController *)controller delta:(NSInteger)delta
583
+ {
584
+ NSInteger index = [_pageControllers indexOfObjectIdenticalTo:controller];
585
+ if (index == NSNotFound) return nil;
586
+ NSInteger target = index + delta;
587
+ if (target < 0 || target >= _pageControllers.count) return nil;
588
+ UIScrollView *scrollView = [self findVerticalScrollViewInView:_pageControllers[target].view];
589
+ if (scrollView != nil) {
590
+ [self applyInsetsToScrollView:scrollView pageIndex:target restore:YES];
591
+ }
592
+ return _pageControllers[target];
593
+ }
594
+
595
+ - (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
596
+ viewControllerAfterViewController:(UIViewController *)viewController
597
+ {
598
+ return [self adjacentController:viewController delta:self.isLtrLayout ? 1 : -1];
599
+ }
600
+
601
+ - (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
602
+ viewControllerBeforeViewController:(UIViewController *)viewController
603
+ {
604
+ return [self adjacentController:viewController delta:self.isLtrLayout ? -1 : 1];
605
+ }
606
+
607
+ - (void)pageViewController:(UIPageViewController *)pageViewController
608
+ didFinishAnimating:(BOOL)finished
609
+ previousViewControllers:(NSArray<UIViewController *> *)previousViewControllers
610
+ transitionCompleted:(BOOL)completed
611
+ {
612
+ _transitioning = NO;
613
+ if (!completed) return;
614
+ UIViewController *controller = _pageViewController.viewControllers.firstObject;
615
+ NSInteger index = [_pageControllers indexOfObjectIdenticalTo:controller];
616
+ if (index == NSNotFound) return;
617
+ [self detachScrollObserver];
618
+ _currentIndex = index;
619
+ [self attachScrollObserverForCurrentPage];
620
+ [self emitPageSelected:index];
621
+ [self emitDiagnostics:@"page-selected"];
622
+ }
623
+
624
+ #pragma mark - Pager UIScrollViewDelegate
625
+
626
+ - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
627
+ {
628
+ const auto emitter = [self eventEmitter];
629
+ if (emitter) {
630
+ emitter->onPageScrollStateChanged({
631
+ .pageScrollState = RNCCollapsiblePagerViewEventEmitter::OnPageScrollStateChangedPageScrollState::Dragging
632
+ });
633
+ }
634
+ }
635
+
636
+ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
637
+ withVelocity:(CGPoint)velocity
638
+ targetContentOffset:(inout CGPoint *)targetContentOffset
639
+ {
640
+ const auto emitter = [self eventEmitter];
641
+ if (emitter) {
642
+ emitter->onPageScrollStateChanged({
643
+ .pageScrollState = RNCCollapsiblePagerViewEventEmitter::OnPageScrollStateChangedPageScrollState::Settling
644
+ });
645
+ }
646
+ }
647
+
648
+ - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
649
+ {
650
+ const auto emitter = [self eventEmitter];
651
+ if (emitter) {
652
+ emitter->onPageScrollStateChanged({
653
+ .pageScrollState = RNCCollapsiblePagerViewEventEmitter::OnPageScrollStateChangedPageScrollState::Idle
654
+ });
655
+ }
656
+ [self emitDiagnostics:@"pager-idle"];
657
+ }
658
+
659
+ - (void)scrollViewDidScroll:(UIScrollView *)scrollView
660
+ {
661
+ CGFloat width = scrollView.bounds.size.width;
662
+ if (width <= 0) return;
663
+ CGFloat rawOffset = (scrollView.contentOffset.x - width) / width;
664
+ BOOL backwards = self.isLtrLayout ? rawOffset < 0 : rawOffset > 0;
665
+ NSInteger position = backwards ? _currentIndex - 1 : _currentIndex;
666
+ CGFloat offset = backwards ? 1 - fabs(rawOffset) : fabs(rawOffset);
667
+ position = MAX(0, MIN(position, (NSInteger)_pageControllers.count - 1));
668
+ const auto emitter = [self eventEmitter];
669
+ if (emitter) {
670
+ emitter->onPageScroll({.position = (double)position, .offset = (double)offset});
671
+ }
672
+ }
673
+
674
+ #pragma mark - Events and commands
675
+
676
+ - (std::shared_ptr<const RNCCollapsiblePagerViewEventEmitter>)eventEmitter
677
+ {
678
+ if (!_eventEmitter) return nullptr;
679
+ return std::static_pointer_cast<const RNCCollapsiblePagerViewEventEmitter>(_eventEmitter);
680
+ }
681
+
682
+ - (void)emitPageSelected:(NSInteger)position
683
+ {
684
+ const auto emitter = [self eventEmitter];
685
+ if (emitter) emitter->onPageSelected({.position = (double)position});
686
+ }
687
+
688
+ - (void)emitDiagnostics:(NSString *)reason
689
+ {
690
+ const auto emitter = [self eventEmitter];
691
+ if (!emitter) return;
692
+ NSInteger attached = 0;
693
+ for (UIViewController *controller in _pageControllers) {
694
+ if (controller.view.superview != nil) attached++;
695
+ }
696
+ emitter->onCollapsibleStateChanged({
697
+ .position = (int)_currentIndex,
698
+ .headerOffset = (double)_headerOffset,
699
+ .nativePageCount = (int)_pageControllers.count,
700
+ .attachedPageCount = (int)attached,
701
+ .observedScrollableCount = (int)_originalInsets.count,
702
+ .retainedPages = std::string(_retainedPages.UTF8String ?: "[]"),
703
+ .reason = std::string(reason.UTF8String ?: "unknown")
704
+ });
705
+ }
706
+
707
+ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
708
+ {
709
+ RCTRNCCollapsiblePagerViewHandleCommand(self, commandName, args);
710
+ }
711
+
712
+ - (void)setPage:(NSInteger)index
713
+ {
714
+ [self goTo:index animated:YES];
715
+ }
716
+
717
+ - (void)setPageWithoutAnimation:(NSInteger)index
718
+ {
719
+ [self goTo:index animated:NO];
720
+ }
721
+
722
+ - (void)setScrollEnabledImperatively:(BOOL)scrollEnabled
723
+ {
724
+ _scrollEnabled = scrollEnabled;
725
+ _pagerScrollView.scrollEnabled = scrollEnabled;
726
+ }
727
+
728
+ + (ComponentDescriptorProvider)componentDescriptorProvider
729
+ {
730
+ return concreteComponentDescriptorProvider<RNCCollapsiblePagerViewComponentDescriptor>();
731
+ }
732
+
733
+ @end
734
+
735
+ Class<RCTComponentViewProtocol> RNCCollapsiblePagerViewCls(void)
736
+ {
737
+ return RNCCollapsiblePagerViewComponentView.class;
738
+ }