1
2#import "ReactNativePageView.h"
3#import "React/RCTLog.h"
4#import <React/RCTViewManager.h>
5
6#import "UIViewController+CreateExtension.h"
7#import "RCTOnPageScrollEvent.h"
8#import "RCTOnPageScrollStateChanged.h"
9#import "RCTOnPageSelected.h"
10#import <math.h>
11
12@interface ReactNativePageView () <UIPageViewControllerDataSource, UIPageViewControllerDelegate, UIScrollViewDelegate, UIGestureRecognizerDelegate>
13
14@property(nonatomic, assign) UIPanGestureRecognizer* panGestureRecognizer;
15
16@property(nonatomic, strong) UIPageViewController *reactPageViewController;
17@property(nonatomic, strong) RCTEventDispatcher *eventDispatcher;
18
19@property(nonatomic, weak) UIScrollView *scrollView;
20@property(nonatomic, weak) UIView *currentView;
21
22@property(nonatomic, strong) NSHashTable<UIViewController *> *cachedControllers;
23@property(nonatomic, assign) CGPoint lastContentOffset;
24
25- (void)goTo:(NSInteger)index animated:(BOOL)animated;
26- (void)shouldScroll:(BOOL)scrollEnabled;
27- (void)shouldDismissKeyboard:(NSString *)dismissKeyboard;
28
29
30@end
31
32@implementation ReactNativePageView {
33    uint16_t _coalescingKey;
34}
35
36- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher {
37    if (self = [super init]) {
38        _scrollEnabled = YES;
39        _pageMargin = 0;
40        _lastReportedIndex = -1;
41        _destinationIndex = -1;
42        _orientation = UIPageViewControllerNavigationOrientationHorizontal;
43        _currentIndex = 0;
44        _dismissKeyboard = UIScrollViewKeyboardDismissModeNone;
45        _coalescingKey = 0;
46        _eventDispatcher = eventDispatcher;
47        _cachedControllers = [NSHashTable hashTableWithOptions:NSHashTableStrongMemory];
48        _overdrag = NO;
49        _layoutDirection = @"ltr";
50        UIPanGestureRecognizer* panGestureRecognizer = [UIPanGestureRecognizer new];
51        self.panGestureRecognizer = panGestureRecognizer;
52        panGestureRecognizer.delegate = self;
53        [self addGestureRecognizer: panGestureRecognizer];
54    }
55    return self;
56}
57
58- (void)layoutSubviews {
59    [super layoutSubviews];
60    if (self.reactPageViewController) {
61        [self shouldScroll:self.scrollEnabled];
62    }
63}
64
65- (void)didUpdateReactSubviews {
66    if (!self.reactPageViewController && self.reactViewController != nil) {
67        [self embed];
68        [self setupInitialController];
69    } else {
70        [self updateDataSource];
71    }
72}
73
74- (void)didMoveToSuperview {
75    [super didMoveToSuperview];
76    if (!self.reactPageViewController && self.reactViewController != nil) {
77        [self embed];
78        [self setupInitialController];
79    }
80}
81
82- (void)didMoveToWindow {
83    [super didMoveToWindow];
84    if (!self.reactPageViewController && self.reactViewController != nil) {
85        [self embed];
86        [self setupInitialController];
87    }
88
89    if (self.reactViewController.navigationController != nil && self.reactViewController.navigationController.interactivePopGestureRecognizer != nil) {
90        [self.scrollView.panGestureRecognizer requireGestureRecognizerToFail:self.reactViewController.navigationController.interactivePopGestureRecognizer];
91    }
92}
93
94- (void)embed {
95    NSDictionary *options = @{ UIPageViewControllerOptionInterPageSpacingKey: @(self.pageMargin) };
96    UIPageViewController *pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
97                                                                               navigationOrientation:self.orientation
98                                                                                             options:options];
99    pageViewController.delegate = self;
100    pageViewController.dataSource = self;
101
102    for (UIView *subview in pageViewController.view.subviews) {
103        if([subview isKindOfClass:UIScrollView.class]){
104            ((UIScrollView *)subview).delegate = self;
105            ((UIScrollView *)subview).keyboardDismissMode = _dismissKeyboard;
106            ((UIScrollView *)subview).delaysContentTouches = YES;
107            self.scrollView = (UIScrollView *)subview;
108        }
109    }
110
111    self.reactPageViewController = pageViewController;
112
113    [self reactAddControllerToClosestParent:pageViewController];
114    [self addSubview:pageViewController.view];
115
116    pageViewController.view.frame = self.bounds;
117
118    [self shouldScroll:self.scrollEnabled];
119
120    [pageViewController.view layoutIfNeeded];
121}
122
123- (void)shouldScroll:(BOOL)scrollEnabled {
124    _scrollEnabled = scrollEnabled;
125    if (self.reactPageViewController.view) {
126        self.scrollView.scrollEnabled = scrollEnabled;
127    }
128}
129
130- (void)shouldDismissKeyboard:(NSString *)dismissKeyboard {
131    _dismissKeyboard = [dismissKeyboard  isEqual: @"on-drag"] ?
132    UIScrollViewKeyboardDismissModeOnDrag : UIScrollViewKeyboardDismissModeNone;
133    self.scrollView.keyboardDismissMode = _dismissKeyboard;
134}
135
136- (void)setupInitialController {
137    UIView *initialView = self.reactSubviews[self.initialPage];
138    if (initialView) {
139        UIViewController *initialController = nil;
140        if (initialView.reactViewController) {
141            initialController = initialView.reactViewController;
142        } else {
143            initialController = [[UIViewController alloc] initWithView:initialView];
144        }
145
146        [self.cachedControllers addObject:initialController];
147
148        [self setReactViewControllers:self.initialPage
149                                 with:initialController
150                            direction:UIPageViewControllerNavigationDirectionForward
151                             animated:YES
152             shouldCallOnPageSelected:YES];
153    }
154}
155
156- (void)setReactViewControllers:(NSInteger)index
157                           with:(UIViewController *)controller
158                      direction:(UIPageViewControllerNavigationDirection)direction
159                       animated:(BOOL)animated
160                       shouldCallOnPageSelected:(BOOL)shouldCallOnPageSelected {
161    if (self.reactPageViewController == nil) {
162        [self enableSwipe];
163        return;
164    }
165
166    NSArray *currentVCs = self.reactPageViewController.viewControllers;
167    if (currentVCs.count == 1 && [currentVCs.firstObject isEqual:controller]) {
168        [self enableSwipe];
169        return;
170    }
171
172    __weak ReactNativePageView *weakSelf = self;
173    uint16_t coalescingKey = _coalescingKey++;
174
175    if (animated == YES) {
176        self.animating = YES;
177    }
178
179    [self.reactPageViewController setViewControllers:@[controller]
180                                           direction:direction
181                                            animated:animated
182                                          completion:^(BOOL finished) {
183        __strong typeof(self) strongSelf = weakSelf;
184        strongSelf.currentIndex = index;
185        strongSelf.currentView = controller.view;
186
187        [strongSelf enableSwipe];
188
189        if (finished) {
190            strongSelf.animating = NO;
191        }
192
193        if (strongSelf.eventDispatcher) {
194            if (strongSelf.lastReportedIndex != strongSelf.currentIndex) {
195                if (shouldCallOnPageSelected) {
196                    [strongSelf.eventDispatcher sendEvent:[[RCTOnPageSelected alloc] initWithReactTag:strongSelf.reactTag position:@(index) coalescingKey:coalescingKey]];
197                }
198                strongSelf.lastReportedIndex = strongSelf.currentIndex;
199            }
200        }
201    }];
202}
203
204- (UIViewController *)currentlyDisplayed {
205    return self.reactPageViewController.viewControllers.firstObject;
206}
207
208- (UIViewController *)findCachedControllerForView:(UIView *)view {
209    for (UIViewController *controller in self.cachedControllers) {
210        if (controller.view.reactTag == view.reactTag) {
211            return controller;
212        }
213    }
214    return nil;
215}
216
217- (void)updateDataSource {
218    if (!self.currentView && self.reactSubviews.count == 0) {
219        return;
220    }
221
222    NSInteger newIndex = self.currentView ? [self.reactSubviews indexOfObject:self.currentView] : 0;
223
224    if (newIndex == NSNotFound) {
225        //Current view was removed
226        NSInteger maxPage = self.reactSubviews.count - 1;
227        NSInteger fallbackIndex = self.currentIndex >= maxPage ? maxPage : self.currentIndex;
228
229        [self goTo:fallbackIndex animated:NO];
230    } else {
231        [self goTo:newIndex animated:NO];
232    }
233}
234
235- (void)disableSwipe {
236    self.reactPageViewController.view.userInteractionEnabled = NO;
237}
238
239- (void)enableSwipe {
240    self.reactPageViewController.view.userInteractionEnabled = YES;
241}
242
243- (void)goTo:(NSInteger)index animated:(BOOL)animated {
244    NSInteger numberOfPages = self.reactSubviews.count;
245
246    [self disableSwipe];
247
248    _destinationIndex = index;
249
250    if (numberOfPages == 0 || index < 0 || index > numberOfPages - 1) {
251        return;
252    }
253
254    BOOL isRTL = ![self isLtrLayout];
255
256    BOOL isForward = (index > self.currentIndex && !isRTL) || (index < self.currentIndex && isRTL);
257
258
259    UIPageViewControllerNavigationDirection direction = isForward ? UIPageViewControllerNavigationDirectionForward : UIPageViewControllerNavigationDirectionReverse;
260
261    long diff = labs(index - _currentIndex);
262
263    [self goToViewController:index direction:direction animated:(!self.animating && animated) shouldCallOnPageSelected: YES];
264
265    if (diff == 0) {
266        [self goToViewController:index direction:direction animated:NO shouldCallOnPageSelected:YES];
267    }
268}
269
270- (void)goToViewController:(NSInteger)index
271                            direction:(UIPageViewControllerNavigationDirection)direction
272                            animated:(BOOL)animated
273                            shouldCallOnPageSelected:(BOOL)shouldCallOnPageSelected {
274    UIView *viewToDisplay = self.reactSubviews[index];
275    UIViewController *controllerToDisplay = [self findAndCacheControllerForView:viewToDisplay];
276    [self setReactViewControllers:index
277                             with:controllerToDisplay
278                        direction:direction
279                         animated:animated
280                        shouldCallOnPageSelected:shouldCallOnPageSelected];
281}
282
283- (UIViewController *)findAndCacheControllerForView:(UIView *)viewToDisplay {
284    if (!viewToDisplay) { return nil; }
285
286    UIViewController *controllerToDisplay = [self findCachedControllerForView:viewToDisplay];
287    UIViewController *current = [self currentlyDisplayed];
288
289    if (!controllerToDisplay && current.view.reactTag == viewToDisplay.reactTag) {
290        controllerToDisplay = current;
291    }
292    if (!controllerToDisplay) {
293        if (viewToDisplay.reactViewController) {
294            controllerToDisplay = viewToDisplay.reactViewController;
295        } else {
296            controllerToDisplay = [[UIViewController alloc] initWithView:viewToDisplay];
297        }
298    }
299    [self.cachedControllers addObject:controllerToDisplay];
300
301    return controllerToDisplay;
302}
303
304- (UIViewController *)nextControllerForController:(UIViewController *)controller
305                                      inDirection:(UIPageViewControllerNavigationDirection)direction {
306    NSUInteger numberOfPages = self.reactSubviews.count;
307    NSInteger index = [self.reactSubviews indexOfObject:controller.view];
308
309    if (index == NSNotFound) {
310        return nil;
311    }
312
313    direction == UIPageViewControllerNavigationDirectionForward ? index++ : index--;
314
315    if (index < 0 || (index > (numberOfPages - 1))) {
316        return nil;
317    }
318
319    UIView *viewToDisplay = self.reactSubviews[index];
320
321    return [self findAndCacheControllerForView:viewToDisplay];
322}
323
324#pragma mark - UIPageViewControllerDelegate
325
326- (void)pageViewController:(UIPageViewController *)pageViewController
327        didFinishAnimating:(BOOL)finished
328   previousViewControllers:(nonnull NSArray<UIViewController *> *)previousViewControllers
329       transitionCompleted:(BOOL)completed {
330
331    if (completed) {
332        UIViewController* currentVC = [self currentlyDisplayed];
333        NSUInteger currentIndex = [self.reactSubviews indexOfObject:currentVC.view];
334
335        self.currentIndex = currentIndex;
336        self.currentView = currentVC.view;
337        [self.eventDispatcher sendEvent:[[RCTOnPageSelected alloc] initWithReactTag:self.reactTag position:@(currentIndex) coalescingKey:_coalescingKey++]];
338        [self.eventDispatcher sendEvent:[[RCTOnPageScrollEvent alloc] initWithReactTag:self.reactTag position:@(currentIndex) offset:@(0.0)]];
339        self.lastReportedIndex = currentIndex;
340    }
341}
342
343#pragma mark - UIPageViewControllerDataSource
344
345- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
346       viewControllerAfterViewController:(UIViewController *)viewController {
347    UIPageViewControllerNavigationDirection direction = [self isLtrLayout] ? UIPageViewControllerNavigationDirectionForward : UIPageViewControllerNavigationDirectionReverse;
348    return [self nextControllerForController:viewController inDirection:direction];
349}
350
351- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
352      viewControllerBeforeViewController:(UIViewController *)viewController {
353    UIPageViewControllerNavigationDirection direction = [self isLtrLayout] ? UIPageViewControllerNavigationDirectionReverse : UIPageViewControllerNavigationDirectionForward;
354    return [self nextControllerForController:viewController inDirection:direction];
355}
356
357#pragma mark - UIPageControlDelegate
358
359- (void)pageControlValueChanged:(UIPageControl *)sender {
360    if (sender.currentPage != self.currentIndex) {
361        [self goTo:sender.currentPage animated:YES];
362    }
363}
364
365#pragma mark - UIScrollViewDelegate
366
367- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
368    [self.eventDispatcher sendEvent:[[RCTOnPageScrollStateChanged alloc] initWithReactTag:self.reactTag state:@"dragging" coalescingKey:_coalescingKey++]];
369}
370
371- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
372    [self.eventDispatcher sendEvent:[[RCTOnPageScrollStateChanged alloc] initWithReactTag:self.reactTag state:@"settling" coalescingKey:_coalescingKey++]];
373
374    if (!_overdrag) {
375        NSInteger maxIndex = self.reactSubviews.count - 1;
376        BOOL isFirstPage = [self isLtrLayout] ? _currentIndex == 0 : _currentIndex == maxIndex;
377        BOOL isLastPage = [self isLtrLayout] ? _currentIndex == maxIndex : _currentIndex == 0;
378        CGFloat contentOffset =[self isHorizontal] ? scrollView.contentOffset.x : scrollView.contentOffset.y;
379        CGFloat topBound = [self isHorizontal] ? scrollView.bounds.size.width : scrollView.bounds.size.height;
380
381        if ((isFirstPage && contentOffset <= topBound) || (isLastPage && contentOffset >= topBound)) {
382            CGPoint croppedOffset = [self isHorizontal] ? CGPointMake(topBound, 0) : CGPointMake(0, topBound);
383            *targetContentOffset = croppedOffset;
384
385            [self.eventDispatcher sendEvent:[[RCTOnPageScrollStateChanged alloc] initWithReactTag:self.reactTag state:@"idle" coalescingKey:_coalescingKey++]];
386        }
387    }
388}
389
390- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
391    [self.eventDispatcher sendEvent:[[RCTOnPageScrollStateChanged alloc] initWithReactTag:self.reactTag state:@"idle" coalescingKey:_coalescingKey++]];
392}
393
394- (BOOL)isHorizontal {
395    return self.orientation == UIPageViewControllerNavigationOrientationHorizontal;
396}
397
398- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
399    CGPoint point = scrollView.contentOffset;
400
401    float offset = 0;
402
403    if (self.isHorizontal) {
404        if (scrollView.frame.size.width != 0) {
405            offset = (point.x - scrollView.frame.size.width)/scrollView.frame.size.width;
406        }
407    } else {
408        if (scrollView.frame.size.height != 0) {
409            offset = (point.y - scrollView.frame.size.height)/scrollView.frame.size.height;
410        }
411    }
412
413    float absoluteOffset = fabs(offset);
414
415    NSInteger position = self.currentIndex;
416
417    BOOL isAnimatingBackwards = ([self isLtrLayout] && offset<0) || (![self isLtrLayout] && offset > 0.05f);
418
419    if (scrollView.isDragging) {
420        _destinationIndex = isAnimatingBackwards ? _currentIndex - 1 : _currentIndex + 1;
421    }
422
423    if(isAnimatingBackwards){
424        position = _destinationIndex;
425        absoluteOffset = fmax(0, 1 - absoluteOffset);
426    }
427
428    if (!_overdrag) {
429        NSInteger maxIndex = self.reactSubviews.count - 1;
430        NSInteger firstPageIndex = [self isLtrLayout] ?  0 :  maxIndex;
431        NSInteger lastPageIndex = [self isLtrLayout] ?  maxIndex :  0;
432        BOOL isFirstPage = _currentIndex == firstPageIndex;
433        BOOL isLastPage = _currentIndex == lastPageIndex;
434        CGFloat contentOffset =[self isHorizontal] ? scrollView.contentOffset.x : scrollView.contentOffset.y;
435        CGFloat topBound = [self isHorizontal] ? scrollView.bounds.size.width : scrollView.bounds.size.height;
436
437        if ((isFirstPage && contentOffset <= topBound) || (isLastPage && contentOffset >= topBound)) {
438            CGPoint croppedOffset = [self isHorizontal] ? CGPointMake(topBound, 0) : CGPointMake(0, topBound);
439            scrollView.contentOffset = croppedOffset;
440            absoluteOffset=0;
441            position = isLastPage ? lastPageIndex : firstPageIndex;
442        }
443    }
444
445    float interpolatedOffset = absoluteOffset * labs(_destinationIndex - _currentIndex);
446
447    self.lastContentOffset = scrollView.contentOffset;
448    [self.eventDispatcher sendEvent:[[RCTOnPageScrollEvent alloc] initWithReactTag:self.reactTag position:@(position) offset:@(interpolatedOffset)]];
449}
450
451- (NSString *)determineScrollDirection:(UIScrollView *)scrollView {
452    NSString *scrollDirection;
453    if (self.isHorizontal) {
454        if (self.lastContentOffset.x > scrollView.contentOffset.x) {
455            scrollDirection = @"left";
456        } else if (self.lastContentOffset.x < scrollView.contentOffset.x) {
457            scrollDirection = @"right";
458        }
459    } else {
460        if (self.lastContentOffset.y > scrollView.contentOffset.y) {
461            scrollDirection = @"up";
462        } else if (self.lastContentOffset.y < scrollView.contentOffset.y) {
463            scrollDirection = @"down";
464        }
465    }
466    return scrollDirection;
467}
468
469- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
470
471    // Recognize simultaneously only if the other gesture is RN Screen's pan gesture (one that is used to perform fullScreenGestureEnabled)
472    if (gestureRecognizer == self.panGestureRecognizer && [NSStringFromClass([otherGestureRecognizer class]) isEqual: @"RNSPanGestureRecognizer"]) {
473        UIPanGestureRecognizer* panGestureRecognizer = (UIPanGestureRecognizer*) gestureRecognizer;
474        CGPoint velocity = [panGestureRecognizer velocityInView:self];
475        BOOL isLTR = [self isLtrLayout];
476        BOOL isBackGesture = (isLTR && velocity.x > 0) || (!isLTR && velocity.x < 0);
477
478        if (self.currentIndex == 0 && isBackGesture) {
479            self.scrollView.panGestureRecognizer.enabled = false;
480        } else {
481            self.scrollView.panGestureRecognizer.enabled = self.scrollEnabled;
482        }
483
484        return YES;
485    }
486
487    self.scrollView.panGestureRecognizer.enabled = self.scrollEnabled;
488    return NO;
489}
490
491- (BOOL)isLtrLayout {
492    return [_layoutDirection isEqualToString:@"ltr"];
493}
494@end
495