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