@onekeyfe/react-native-pager-view 1.1.35

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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +306 -0
  3. package/android/Android.mk +45 -0
  4. package/android/build.gradle +237 -0
  5. package/android/debug.keystore +0 -0
  6. package/android/gradle.properties +5 -0
  7. package/android/registration.cpp +18 -0
  8. package/android/src/main/AndroidManifest.xml +4 -0
  9. package/android/src/main/java/com/reactnativepagerview/NestedScrollableHost.kt +148 -0
  10. package/android/src/main/java/com/reactnativepagerview/PagerViewViewManager.kt +222 -0
  11. package/android/src/main/java/com/reactnativepagerview/PagerViewViewManagerImpl.kt +228 -0
  12. package/android/src/main/java/com/reactnativepagerview/PagerViewViewPackage.kt +17 -0
  13. package/android/src/main/java/com/reactnativepagerview/ViewPagerAdapter.kt +121 -0
  14. package/android/src/main/java/com/reactnativepagerview/ViewPagerViewHolder.kt +21 -0
  15. package/android/src/main/java/com/reactnativepagerview/event/PageScrollEvent.kt +47 -0
  16. package/android/src/main/java/com/reactnativepagerview/event/PageScrollStateChangedEvent.kt +34 -0
  17. package/android/src/main/java/com/reactnativepagerview/event/PageSelectedEvent.kt +38 -0
  18. package/ios/PagerView.xcodeproj/project.pbxproj +274 -0
  19. package/ios/RCTOnPageScrollEvent.h +14 -0
  20. package/ios/RCTOnPageScrollEvent.m +60 -0
  21. package/ios/RNCPagerViewComponentView.h +11 -0
  22. package/ios/RNCPagerViewComponentView.mm +704 -0
  23. package/lib/module/PagerView.js +136 -0
  24. package/lib/module/PagerView.js.map +1 -0
  25. package/lib/module/PagerViewNativeComponent.ts +82 -0
  26. package/lib/module/codegen-types.d.js +2 -0
  27. package/lib/module/codegen-types.d.js.map +1 -0
  28. package/lib/module/index.js +6 -0
  29. package/lib/module/index.js.map +1 -0
  30. package/lib/module/package.json +1 -0
  31. package/lib/module/usePagerView.js +106 -0
  32. package/lib/module/usePagerView.js.map +1 -0
  33. package/lib/module/utils.js +27 -0
  34. package/lib/module/utils.js.map +1 -0
  35. package/lib/typescript/package.json +1 -0
  36. package/lib/typescript/src/PagerView.d.ts +70 -0
  37. package/lib/typescript/src/PagerView.d.ts.map +1 -0
  38. package/lib/typescript/src/PagerViewNativeComponent.d.ts +51 -0
  39. package/lib/typescript/src/PagerViewNativeComponent.d.ts.map +1 -0
  40. package/lib/typescript/src/index.d.ts +10 -0
  41. package/lib/typescript/src/index.d.ts.map +1 -0
  42. package/lib/typescript/src/usePagerView.d.ts +38 -0
  43. package/lib/typescript/src/usePagerView.d.ts.map +1 -0
  44. package/lib/typescript/src/utils.d.ts +3 -0
  45. package/lib/typescript/src/utils.d.ts.map +1 -0
  46. package/package.json +101 -0
  47. package/react-native-pager-view.podspec +20 -0
  48. package/src/PagerView.tsx +170 -0
  49. package/src/PagerViewNativeComponent.ts +82 -0
  50. package/src/codegen-types.d.ts +28 -0
  51. package/src/index.tsx +27 -0
  52. package/src/usePagerView.ts +148 -0
  53. package/src/utils.tsx +22 -0
@@ -0,0 +1,704 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import "RNCPagerViewComponentView.h"
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 "RCTFabricComponentsPlugins.h"
9
+ #import "React/RCTConversions.h"
10
+
11
+ #import "RCTOnPageScrollEvent.h"
12
+
13
+ using namespace facebook::react;
14
+
15
+ @interface RNCPagerViewComponentView () <RCTRNCViewPagerViewProtocol, UIPageViewControllerDataSource, UIPageViewControllerDelegate, UIScrollViewDelegate, UIGestureRecognizerDelegate>
16
+
17
+ @property(nonatomic, strong) UIPageViewController *nativePageViewController;
18
+ @property(nonatomic, strong) NSMutableArray<UIViewController *> *nativeChildrenViewControllers;
19
+
20
+ @end
21
+
22
+ @implementation RNCPagerViewComponentView {
23
+ LayoutMetrics _layoutMetrics;
24
+ LayoutMetrics _oldLayoutMetrics;
25
+ UIScrollView *scrollView;
26
+ BOOL transitioning;
27
+ NSInteger _currentIndex;
28
+ NSInteger _destinationIndex;
29
+ BOOL _overdrag;
30
+ NSString *_layoutDirection;
31
+ BOOL _scrollEnabled;
32
+ // _isBeingRecycled: synchronous guard, only true during prepareForRecycle execution
33
+ BOOL _isBeingRecycled;
34
+ // _generation: async guard for recycle-and-reuse, incremented in prepareForRecycle
35
+ NSUInteger _generation;
36
+ // _transitionId: async guard for superseded transitions, incremented each setPagerViewControllers call
37
+ NSUInteger _transitionId;
38
+ // _pendingGoToIndex: stores the latest blocked animated transition target, drained after current transition completes
39
+ NSInteger _pendingGoToIndex;
40
+ // _nestedScrollEnabled: when YES, this inner PagerView coordinates gestures with an outer PagerView
41
+ BOOL _nestedScrollEnabled;
42
+ // _blockerGesture: gate gesture that blocks inner scroll at page boundaries when nested
43
+ UIPanGestureRecognizer *_blockerGesture;
44
+ }
45
+
46
+ // Needed because of this: https://github.com/facebook/react-native/pull/37274
47
+ + (void)load
48
+ {
49
+ [super load];
50
+ }
51
+
52
+ - (void)initializeNativePageViewController {
53
+ const auto &viewProps = *std::static_pointer_cast<const RNCViewPagerProps>(_props);
54
+ NSDictionary *options = @{ UIPageViewControllerOptionInterPageSpacingKey: @(viewProps.pageMargin) };
55
+ UIPageViewControllerNavigationOrientation orientation = UIPageViewControllerNavigationOrientationHorizontal;
56
+ switch (viewProps.orientation) {
57
+ case RNCViewPagerOrientation::Horizontal:
58
+ orientation = UIPageViewControllerNavigationOrientationHorizontal;
59
+ break;
60
+ case RNCViewPagerOrientation::Vertical:
61
+ orientation = UIPageViewControllerNavigationOrientationVertical;
62
+ break;
63
+ }
64
+ _nativePageViewController = [[UIPageViewController alloc]
65
+ initWithTransitionStyle: UIPageViewControllerTransitionStyleScroll
66
+ navigationOrientation:orientation
67
+ options:options];
68
+ _nativePageViewController.dataSource = self;
69
+ _nativePageViewController.delegate = self;
70
+ _nativePageViewController.view.frame = self.frame;
71
+ self.contentView = _nativePageViewController.view;
72
+
73
+ for (UIView *subview in _nativePageViewController.view.subviews) {
74
+ if([subview isKindOfClass:UIScrollView.class]){
75
+ ((UIScrollView *)subview).delegate = self;
76
+ ((UIScrollView *)subview).delaysContentTouches = NO;
77
+ scrollView = (UIScrollView *)subview;
78
+ }
79
+ }
80
+
81
+ [self applyScrollEnabled];
82
+ [self applyNestedScrollBlocker];
83
+ }
84
+
85
+ - (instancetype)initWithFrame:(CGRect)frame
86
+ {
87
+ if (self = [super initWithFrame:frame]) {
88
+ static const auto defaultProps = std::make_shared<const RNCViewPagerProps>();
89
+ _props = defaultProps;
90
+ _nativeChildrenViewControllers = [[NSMutableArray alloc] init];
91
+ _currentIndex = -1;
92
+ _destinationIndex = -1;
93
+ _layoutDirection = @"ltr";
94
+ _overdrag = NO;
95
+ _scrollEnabled = YES;
96
+ _isBeingRecycled = NO;
97
+ _pendingGoToIndex = -1;
98
+ _nestedScrollEnabled = NO;
99
+ }
100
+
101
+ return self;
102
+ }
103
+
104
+ - (void)willMoveToSuperview:(UIView *)newSuperview {
105
+ if (newSuperview != nil) {
106
+ [self initializeNativePageViewController];
107
+ [self goTo:_currentIndex animated:NO];
108
+ }
109
+ }
110
+
111
+
112
+ #pragma mark - React API
113
+
114
+ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
115
+ if (_isBeingRecycled) {
116
+ return;
117
+ }
118
+ UIViewController *vc = [UIViewController new];
119
+ [vc.view addSubview:childComponentView];
120
+ [_nativeChildrenViewControllers insertObject:vc atIndex:index];
121
+ [self goTo:_currentIndex animated:NO];
122
+ }
123
+
124
+ - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
125
+ if (_isBeingRecycled) {
126
+ return;
127
+ }
128
+
129
+ if (index >= _nativeChildrenViewControllers.count) {
130
+ return;
131
+ }
132
+
133
+ [childComponentView removeFromSuperview];
134
+ [_nativeChildrenViewControllers removeObjectAtIndex:index];
135
+
136
+ NSInteger maxPage = _nativeChildrenViewControllers.count - 1;
137
+
138
+ if (_currentIndex >= maxPage) {
139
+ if (maxPage >= 0) {
140
+ [self goTo:maxPage animated:NO];
141
+ }
142
+ }
143
+ }
144
+
145
+
146
+ -(void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics {
147
+ _oldLayoutMetrics = oldLayoutMetrics;
148
+ _layoutMetrics = layoutMetrics;
149
+
150
+ if (transitioning) {
151
+ return;
152
+ }
153
+
154
+ [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:_oldLayoutMetrics];
155
+ }
156
+
157
+
158
+ -(void)prepareForRecycle {
159
+ _isBeingRecycled = YES;
160
+ _generation++;
161
+
162
+ // Cancel any ongoing transitions
163
+ if (_nativePageViewController != nil && transitioning) {
164
+ transitioning = NO;
165
+ }
166
+
167
+ // Clear view controllers before recycling
168
+ [_nativeChildrenViewControllers removeAllObjects];
169
+
170
+ // Remove blocker gesture before recycling
171
+ if (_blockerGesture != nil) {
172
+ [self removeGestureRecognizer:_blockerGesture];
173
+ _blockerGesture = nil;
174
+ }
175
+
176
+ [super prepareForRecycle];
177
+ _nativePageViewController = nil;
178
+ _currentIndex = -1;
179
+ _scrollEnabled = YES;
180
+ _pendingGoToIndex = -1;
181
+ _nestedScrollEnabled = NO;
182
+ _isBeingRecycled = NO;
183
+ }
184
+
185
+ - (void)shouldDismissKeyboard:(RNCViewPagerKeyboardDismissMode)dismissKeyboard {
186
+ #if !TARGET_OS_VISION
187
+ UIScrollViewKeyboardDismissMode dismissKeyboardMode = UIScrollViewKeyboardDismissModeNone;
188
+ switch (dismissKeyboard) {
189
+ case RNCViewPagerKeyboardDismissMode::None:
190
+ dismissKeyboardMode = UIScrollViewKeyboardDismissModeNone;
191
+ break;
192
+ case RNCViewPagerKeyboardDismissMode::OnDrag:
193
+ dismissKeyboardMode = UIScrollViewKeyboardDismissModeOnDrag;
194
+ break;
195
+ }
196
+ scrollView.keyboardDismissMode = dismissKeyboardMode;
197
+ #endif
198
+ }
199
+
200
+ - (void)applyScrollEnabled {
201
+ scrollView.scrollEnabled = _scrollEnabled;
202
+ }
203
+
204
+
205
+ - (void)updateProps:(const facebook::react::Props::Shared &)props oldProps:(const facebook::react::Props::Shared &)oldProps{
206
+ const auto &oldScreenProps = *std::static_pointer_cast<const RNCViewPagerProps>(_props);
207
+ const auto &newScreenProps = *std::static_pointer_cast<const RNCViewPagerProps>(props);
208
+
209
+ // change index only once
210
+ if (_currentIndex == -1) {
211
+ _currentIndex = newScreenProps.initialPage;
212
+ [self shouldDismissKeyboard: newScreenProps.keyboardDismissMode];
213
+ }
214
+
215
+ const auto newLayoutDirectionStr = RCTNSStringFromString(toString(newScreenProps.layoutDirection));
216
+
217
+
218
+ if (_layoutDirection != newLayoutDirectionStr) {
219
+ _layoutDirection = newLayoutDirectionStr;
220
+ }
221
+
222
+ if (oldScreenProps.keyboardDismissMode != newScreenProps.keyboardDismissMode) {
223
+ [self shouldDismissKeyboard: newScreenProps.keyboardDismissMode];
224
+ }
225
+
226
+ if (oldScreenProps.scrollEnabled != newScreenProps.scrollEnabled) {
227
+ _scrollEnabled = newScreenProps.scrollEnabled;
228
+ [self applyScrollEnabled];
229
+ }
230
+
231
+ if (newScreenProps.overdrag != _overdrag) {
232
+ _overdrag = newScreenProps.overdrag;
233
+ }
234
+
235
+ if (oldScreenProps.nestedScrollEnabled != newScreenProps.nestedScrollEnabled) {
236
+ _nestedScrollEnabled = newScreenProps.nestedScrollEnabled;
237
+ [self applyNestedScrollBlocker];
238
+ }
239
+
240
+ [super updateProps:props oldProps:oldProps];
241
+ }
242
+
243
+
244
+ #pragma mark - Internal methods
245
+
246
+ - (void)disableSwipe {
247
+ self.nativePageViewController.view.userInteractionEnabled = NO;
248
+ }
249
+
250
+ - (void)enableSwipe {
251
+ self.nativePageViewController.view.userInteractionEnabled = YES;
252
+ }
253
+
254
+ - (void)goTo:(NSInteger)index animated:(BOOL)animated {
255
+ NSInteger numberOfPages = _nativeChildrenViewControllers.count;
256
+
257
+ // Check recycling state BEFORE disableSwipe to avoid state inconsistency
258
+ if (_isBeingRecycled) {
259
+ #ifdef DEBUG
260
+ NSLog(@"[PagerView] goTo blocked: view is being recycled (index=%ld)", (long)index);
261
+ #endif
262
+ return;
263
+ }
264
+
265
+ [self disableSwipe];
266
+
267
+ _destinationIndex = index;
268
+
269
+
270
+ if (numberOfPages == 0 || index < 0 || index > numberOfPages - 1) {
271
+ [self enableSwipe];
272
+ return;
273
+ }
274
+
275
+ BOOL isForward = (index > _currentIndex && [self isLtrLayout]) || (index < _currentIndex && ![self isLtrLayout]);
276
+ UIPageViewControllerNavigationDirection direction = isForward ? UIPageViewControllerNavigationDirectionForward : UIPageViewControllerNavigationDirectionReverse;
277
+
278
+ long diff = labs(index - _currentIndex);
279
+
280
+ [self setPagerViewControllers:index
281
+ direction:direction
282
+ animated:diff == 0 ? NO : animated];
283
+
284
+ }
285
+
286
+ - (void)setPagerViewControllers:(NSInteger)index
287
+ direction:(UIPageViewControllerNavigationDirection)direction
288
+ animated:(BOOL)animated{
289
+ if (_nativePageViewController == nil || _isBeingRecycled) {
290
+ #ifdef DEBUG
291
+ NSLog(@"[PagerView] setPagerViewControllers blocked: pageVC=%@ recycled=%d index=%ld",
292
+ _nativePageViewController ? @"EXISTS" : @"NIL", _isBeingRecycled, (long)index);
293
+ #endif
294
+ [self enableSwipe];
295
+ return;
296
+ }
297
+
298
+ // Validate index bounds
299
+ if (index < 0 || index >= _nativeChildrenViewControllers.count) {
300
+ #ifdef DEBUG
301
+ NSLog(@"[PagerView] setPagerViewControllers blocked: index %ld out of bounds (count=%lu)",
302
+ (long)index, (unsigned long)_nativeChildrenViewControllers.count);
303
+ #endif
304
+ [self enableSwipe];
305
+ return;
306
+ }
307
+
308
+ // Validate view controller and its view
309
+ UIViewController *targetVC = [_nativeChildrenViewControllers objectAtIndex:index];
310
+ if (targetVC == nil || targetVC.view == nil) {
311
+ #ifdef DEBUG
312
+ NSLog(@"[PagerView] setPagerViewControllers blocked: targetVC=%@ view=%@ index=%ld",
313
+ targetVC ? @"EXISTS" : @"NIL", targetVC.view ? @"EXISTS" : @"NIL", (long)index);
314
+ #endif
315
+ [self enableSwipe];
316
+ return;
317
+ }
318
+
319
+ // Note: Do NOT check superview/window == nil here!
320
+ // Target page views are expected to have nil superview/window before being added to pageViewController
321
+ // Checking for nil superview/window would block all legitimate page transitions
322
+
323
+ // Prevent calling setViewControllers during an ongoing animated transition.
324
+ // UIPageViewController's internal _UIQueuingScrollView does not support
325
+ // concurrent enqueued transitions and will crash with
326
+ // "Duplicate states in queue" (NSInternalInconsistencyException).
327
+ // Note: Non-animated transitions (animated=NO) are synchronous and bypass
328
+ // the internal _UIQueuingScrollView queue, so they are safe during an ongoing transition.
329
+ if (transitioning && animated) {
330
+ #ifdef DEBUG
331
+ NSLog(@"[PagerView] setPagerViewControllers deferred: already transitioning (index=%ld)", (long)index);
332
+ #endif
333
+ _pendingGoToIndex = index;
334
+ [self enableSwipe];
335
+ return;
336
+ }
337
+
338
+ // Clear any pending transition since we are now starting a fresh one
339
+ _pendingGoToIndex = -1;
340
+
341
+ transitioning = YES;
342
+
343
+ __weak RNCPagerViewComponentView *weakSelf = self;
344
+ NSUInteger capturedGeneration = _generation;
345
+ _transitionId++;
346
+ NSUInteger capturedTransitionId = _transitionId;
347
+ [_nativePageViewController setViewControllers:@[targetVC]
348
+ direction:direction
349
+ animated:animated
350
+ completion:^(BOOL finished) {
351
+ __strong RNCPagerViewComponentView *strongSelf = weakSelf;
352
+ if (strongSelf == nil) {
353
+ return;
354
+ }
355
+
356
+ // View was recycled and possibly reused since this animation started
357
+ if (strongSelf->_isBeingRecycled || strongSelf->_generation != capturedGeneration) {
358
+ // Do NOT reset transitioning here — a new lifecycle owns that state
359
+ return;
360
+ }
361
+
362
+ // A newer transition was started, this completion is stale
363
+ if (strongSelf->_transitionId != capturedTransitionId) {
364
+ // Do NOT reset transitioning — the newer transition owns that state
365
+ return;
366
+ }
367
+
368
+ // Only reset transitioning when this completion corresponds to the current transition
369
+ strongSelf->transitioning = NO;
370
+
371
+ [strongSelf enableSwipe];
372
+ strongSelf->_currentIndex = index;
373
+ if (strongSelf->_eventEmitter != nullptr ) {
374
+ const auto eventEmitter = [strongSelf pagerEventEmitter];
375
+ if (eventEmitter) {
376
+ int position = (int) index;
377
+ eventEmitter->onPageSelected(RNCViewPagerEventEmitter::OnPageSelected{.position = static_cast<double>(position)});
378
+ }
379
+ }
380
+ [strongSelf updateLayoutMetrics:strongSelf->_layoutMetrics oldLayoutMetrics:strongSelf->_oldLayoutMetrics];
381
+
382
+ // Drain pending transition that was blocked while this animation was in-flight
383
+ NSInteger pending = strongSelf->_pendingGoToIndex;
384
+ if (pending >= 0 && pending != index) {
385
+ strongSelf->_pendingGoToIndex = -1;
386
+ [strongSelf goTo:pending animated:YES];
387
+ }
388
+ }];
389
+ }
390
+
391
+
392
+ - (UIViewController *)nextControllerForController:(UIViewController *)controller
393
+ inDirection:(UIPageViewControllerNavigationDirection)direction {
394
+ NSUInteger numberOfPages = _nativeChildrenViewControllers.count;
395
+
396
+ if (_isBeingRecycled || numberOfPages == 0) {
397
+ return nil;
398
+ }
399
+
400
+ NSInteger index = [_nativeChildrenViewControllers indexOfObject:controller];
401
+
402
+ if (index == NSNotFound) {
403
+ return nil;
404
+ }
405
+
406
+ direction == UIPageViewControllerNavigationDirectionForward ? index++ : index--;
407
+
408
+ if (index < 0 || (index > (numberOfPages - 1))) {
409
+ return nil;
410
+ }
411
+
412
+ return [_nativeChildrenViewControllers objectAtIndex:index];
413
+ }
414
+
415
+ - (UIViewController *)currentlyDisplayed {
416
+ return _nativePageViewController.viewControllers.firstObject;
417
+ }
418
+
419
+ #pragma mark - UIScrollViewDelegate
420
+
421
+ - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
422
+ if (_isBeingRecycled) {
423
+ return;
424
+ }
425
+ const auto eventEmitter = [self pagerEventEmitter];
426
+ if (!eventEmitter) {
427
+ return;
428
+ }
429
+ eventEmitter->onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{.pageScrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Dragging });
430
+ }
431
+
432
+ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
433
+ if (_isBeingRecycled) {
434
+ return;
435
+ }
436
+ const auto eventEmitter = [self pagerEventEmitter];
437
+ if (!eventEmitter) {
438
+ return;
439
+ }
440
+ eventEmitter->onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{.pageScrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Settling });
441
+
442
+ if (!_overdrag) {
443
+ NSInteger maxIndex = _nativeChildrenViewControllers.count - 1;
444
+ BOOL isFirstPage = [self isLtrLayout] ? _currentIndex == 0 : _currentIndex == maxIndex;
445
+ BOOL isLastPage = [self isLtrLayout] ? _currentIndex == maxIndex : _currentIndex == 0;
446
+ CGFloat contentOffset = [self isHorizontal] ? scrollView.contentOffset.x : scrollView.contentOffset.y;
447
+ CGFloat topBound = [self isHorizontal] ? scrollView.bounds.size.width : scrollView.bounds.size.height;
448
+
449
+ if ((isFirstPage && contentOffset <= topBound) || (isLastPage && contentOffset >= topBound)) {
450
+ CGPoint croppedOffset = [self isHorizontal] ? CGPointMake(topBound, 0) : CGPointMake(0, topBound);
451
+ *targetContentOffset = croppedOffset;
452
+
453
+ eventEmitter->onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{.pageScrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Idle });
454
+ }
455
+ }
456
+ }
457
+
458
+ - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
459
+ if (_isBeingRecycled) {
460
+ return;
461
+ }
462
+ const auto eventEmitter = [self pagerEventEmitter];
463
+ if (!eventEmitter) {
464
+ return;
465
+ }
466
+ eventEmitter->onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{.pageScrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Idle });
467
+ }
468
+
469
+
470
+ - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
471
+ if (_isBeingRecycled) {
472
+ return;
473
+ }
474
+ BOOL isHorizontal = [self isHorizontal];
475
+ CGFloat contentOffset = isHorizontal ? scrollView.contentOffset.x : scrollView.contentOffset.y;
476
+ CGFloat frameSize = isHorizontal ? scrollView.frame.size.width : scrollView.frame.size.height;
477
+
478
+ if (frameSize == 0) {
479
+ return;
480
+ }
481
+
482
+ float offset = (contentOffset - frameSize) / frameSize;
483
+ float absoluteOffset = fabs(offset);
484
+ NSInteger position = _currentIndex;
485
+
486
+ BOOL isHorizontalRtl = [self isHorizontalRtlLayout];
487
+ BOOL isAnimatingBackwards = isHorizontalRtl ? offset > 0.05f : offset < 0;
488
+ BOOL isBeingMovedByNestedScrollView = !scrollView.isDragging && !scrollView.isTracking;
489
+ if (scrollView.isDragging || isBeingMovedByNestedScrollView) {
490
+ _destinationIndex = isAnimatingBackwards ? _currentIndex - 1 : _currentIndex + 1;
491
+ }
492
+
493
+ if (isAnimatingBackwards) {
494
+ position = _destinationIndex;
495
+ absoluteOffset = fmax(0, 1 - absoluteOffset);
496
+ }
497
+
498
+ if (!_overdrag) {
499
+ NSInteger maxIndex = _nativeChildrenViewControllers.count - 1;
500
+ NSInteger firstPageIndex = isHorizontalRtl ? maxIndex : 0;
501
+ NSInteger lastPageIndex = isHorizontalRtl ? 0 : maxIndex;
502
+ BOOL isFirstPage = _currentIndex == firstPageIndex;
503
+ BOOL isLastPage = _currentIndex == lastPageIndex;
504
+ CGFloat topBound = isHorizontal ? scrollView.bounds.size.width : scrollView.bounds.size.height;
505
+
506
+ if ((isFirstPage && contentOffset <= topBound) || (isLastPage && contentOffset >= topBound)) {
507
+ CGPoint croppedOffset = isHorizontal ? CGPointMake(topBound, 0) : CGPointMake(0, topBound);
508
+ scrollView.contentOffset = croppedOffset;
509
+ absoluteOffset = 0;
510
+ position = isLastPage ? lastPageIndex : firstPageIndex;
511
+ }
512
+ }
513
+
514
+ float interpolatedOffset = absoluteOffset * labs(_destinationIndex - _currentIndex);
515
+ [self sendScrollEventsForPosition:position offset:interpolatedOffset];
516
+ }
517
+
518
+
519
+ #pragma mark - UIPageViewControllerDelegate
520
+
521
+ - (void)pageViewController:(UIPageViewController *)pageViewController
522
+ didFinishAnimating:(BOOL)finished
523
+ previousViewControllers:(nonnull NSArray<UIViewController *> *)previousViewControllers
524
+ transitionCompleted:(BOOL)completed {
525
+ if (_isBeingRecycled) {
526
+ return;
527
+ }
528
+ if (completed) {
529
+ UIViewController* currentVC = [self currentlyDisplayed];
530
+ NSUInteger currentIndex = [_nativeChildrenViewControllers indexOfObject:currentVC];
531
+ _currentIndex = currentIndex;
532
+ int position = (int) currentIndex;
533
+ const auto eventEmitter = [self pagerEventEmitter];
534
+ if (eventEmitter) {
535
+ eventEmitter->onPageSelected(RNCViewPagerEventEmitter::OnPageSelected{.position = static_cast<double>(position)});
536
+ }
537
+ }
538
+ }
539
+
540
+ #pragma mark - UIPageViewControllerDataSource
541
+
542
+ - (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
543
+ viewControllerAfterViewController:(UIViewController *)viewController {
544
+
545
+ UIPageViewControllerNavigationDirection direction = [self isLtrLayout] ? UIPageViewControllerNavigationDirectionForward : UIPageViewControllerNavigationDirectionReverse;
546
+ return [self nextControllerForController:viewController inDirection:direction];
547
+ }
548
+
549
+ - (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
550
+ viewControllerBeforeViewController:(UIViewController *)viewController {
551
+ UIPageViewControllerNavigationDirection direction = [self isLtrLayout] ? UIPageViewControllerNavigationDirectionReverse : UIPageViewControllerNavigationDirectionForward;
552
+ return [self nextControllerForController:viewController inDirection:direction];
553
+ }
554
+
555
+ #pragma mark - Imperative methods exposed to React Native
556
+
557
+ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
558
+ RCTRNCViewPagerHandleCommand(self, commandName, args);
559
+ }
560
+
561
+ - (void)setPage:(NSInteger)index {
562
+ [self goTo:index animated:YES];
563
+ }
564
+
565
+ - (void)setPageWithoutAnimation:(NSInteger)index {
566
+ [self goTo:index animated:NO];
567
+ }
568
+
569
+ - (void)setScrollEnabledImperatively:(BOOL)scrollEnabled {
570
+ _scrollEnabled = scrollEnabled;
571
+ [self applyScrollEnabled];
572
+ }
573
+
574
+ #pragma mark - Nested Scroll Gesture Coordination
575
+
576
+ - (void)applyNestedScrollBlocker {
577
+ if (scrollView == nil) return;
578
+
579
+ if (_nestedScrollEnabled && _blockerGesture == nil) {
580
+ _blockerGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(blockerGestureFired:)];
581
+ _blockerGesture.delegate = self;
582
+ // Cancel touches so the blocker never actually scrolls anything
583
+ _blockerGesture.cancelsTouchesInView = NO;
584
+ _blockerGesture.delaysTouchesBegan = NO;
585
+ [self addGestureRecognizer:_blockerGesture];
586
+ // Inner scroll view pan must wait for the blocker to fail before it can begin
587
+ [scrollView.panGestureRecognizer requireGestureRecognizerToFail:_blockerGesture];
588
+ } else if (!_nestedScrollEnabled && _blockerGesture != nil) {
589
+ [self removeGestureRecognizer:_blockerGesture];
590
+ _blockerGesture = nil;
591
+ }
592
+ }
593
+
594
+ // The blocker gesture never actually does anything — it only serves as a gate
595
+ - (void)blockerGestureFired:(UIPanGestureRecognizer *)recognizer {
596
+ // Immediately cancel so it doesn't interfere with other gestures
597
+ if (recognizer.state == UIGestureRecognizerStateBegan) {
598
+ recognizer.state = UIGestureRecognizerStateCancelled;
599
+ }
600
+ }
601
+
602
+ #pragma mark - UIGestureRecognizerDelegate
603
+
604
+ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
605
+ if (gestureRecognizer != _blockerGesture) return YES;
606
+ if (!_nestedScrollEnabled) return NO;
607
+ if (![gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) return NO;
608
+
609
+ UIPanGestureRecognizer *pan = (UIPanGestureRecognizer *)gestureRecognizer;
610
+ CGPoint velocity = [pan velocityInView:self];
611
+ NSInteger pageCount = _nativeChildrenViewControllers.count;
612
+
613
+ if (pageCount == 0) return NO;
614
+ if (_currentIndex < 0 || _currentIndex >= pageCount) return NO;
615
+
616
+ // Nested coordination is designed for horizontal inner/outer pagers only.
617
+ // For vertical pagers, do not block so inner scrolling keeps working.
618
+ BOOL isHorizontal = [self isHorizontal];
619
+ if (!isHorizontal) return NO;
620
+
621
+ // Only edge-block for gestures that are primarily horizontal.
622
+ // This avoids accidental outer-tab switches on vertical/diagonal content scrolls.
623
+ CGFloat absVelocityX = fabs(velocity.x);
624
+ CGFloat absVelocityY = fabs(velocity.y);
625
+ if (absVelocityX <= absVelocityY) return NO;
626
+ if (absVelocityX < 1.0f) return NO;
627
+
628
+ BOOL isLtr = [self isLtrLayout];
629
+ NSInteger firstPageIndex = isLtr ? 0 : pageCount - 1;
630
+ NSInteger lastPageIndex = isLtr ? pageCount - 1 : 0;
631
+
632
+ // At first page, swiping backward → blocker begins → inner scroll blocked → outer takes over
633
+ if (_currentIndex == firstPageIndex && velocity.x > 0) return YES;
634
+ // At last page, swiping forward → blocker begins → inner scroll blocked → outer takes over
635
+ if (_currentIndex == lastPageIndex && velocity.x < 0) return YES;
636
+
637
+ // Not at boundary or swiping inward → blocker fails → inner scroll works normally
638
+ return NO;
639
+ }
640
+
641
+ // Allow the blocker to be recognized simultaneously so it doesn't steal from vertical scrolls
642
+ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
643
+ if (gestureRecognizer == _blockerGesture) return YES;
644
+ return NO;
645
+ }
646
+
647
+ #pragma mark - Helpers
648
+
649
+ - (BOOL)isHorizontalRtlLayout {
650
+ return self.isHorizontal && !self.isLtrLayout;
651
+ }
652
+
653
+ - (BOOL)isHorizontal {
654
+ return _nativePageViewController.navigationOrientation == UIPageViewControllerNavigationOrientationHorizontal;
655
+ }
656
+
657
+ - (BOOL)isLtrLayout {
658
+ return [_layoutDirection isEqualToString: @"ltr"];
659
+ }
660
+
661
+ - (std::shared_ptr<const RNCViewPagerEventEmitter>)pagerEventEmitter
662
+ {
663
+ if (!_eventEmitter) {
664
+ return nullptr;
665
+ }
666
+
667
+ assert(std::dynamic_pointer_cast<const RNCViewPagerEventEmitter>(_eventEmitter));
668
+ return std::static_pointer_cast<const RNCViewPagerEventEmitter>(_eventEmitter);
669
+ }
670
+
671
+ - (void)sendScrollEventsForPosition:(NSInteger)position offset:(CGFloat)offset {
672
+ const auto eventEmitter = [self pagerEventEmitter];
673
+ if (eventEmitter) {
674
+ eventEmitter->onPageScroll(RNCViewPagerEventEmitter::OnPageScroll{
675
+ .position = static_cast<double>(position),
676
+ .offset = offset
677
+ });
678
+ }
679
+
680
+ // This is temporary workaround to allow animations based on onPageScroll event
681
+ // until Fabric implements proper NativeAnimationDriver,
682
+ // see: https://github.com/facebook/react-native/blob/44f431b471c243c92284aa042d3807ba4d04af65/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm#L59
683
+ RCTOnPageScrollEvent *event = [[RCTOnPageScrollEvent alloc] initWithReactTag:@(self.tag)
684
+ position:@(position)
685
+ offset:@(offset)];
686
+ NSDictionary *userInfo = @{@"event": event};
687
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"RCTNotifyEventDispatcherObserversOfEvent_DEPRECATED"
688
+ object:nil
689
+ userInfo:userInfo];
690
+ }
691
+
692
+ #pragma mark - RCTComponentViewProtocol
693
+
694
+ + (ComponentDescriptorProvider)componentDescriptorProvider
695
+ {
696
+ return concreteComponentDescriptorProvider<RNCViewPagerComponentDescriptor>();
697
+ }
698
+
699
+ @end
700
+
701
+ Class<RCTComponentViewProtocol> RNCViewPagerCls(void)
702
+ {
703
+ return RNCPagerViewComponentView.class;
704
+ }