UNPKG

16.1 kBPlain TextView Raw
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
11@interface ReactNativePageView () <UIPageViewControllerDataSource, UIPageViewControllerDelegate, UIScrollViewDelegate>
12
13@property(nonatomic, strong) UIPageViewController *reactPageViewController;
14@property(nonatomic, strong) UIPageControl *reactPageIndicatorView;
15@property(nonatomic, strong) RCTEventDispatcher *eventDispatcher;
16
17@property(nonatomic, weak) UIScrollView *scrollView;
18@property(nonatomic, weak) UIView *currentView;
19
20@property(nonatomic, strong) NSHashTable<UIViewController *> *cachedControllers;
21@property (nonatomic, assign) CGPoint lastContentOffset;
22
23- (void)goTo:(NSInteger)index animated:(BOOL)animated;
24- (void)shouldScroll:(BOOL)scrollEnabled;
25- (void)shouldShowPageIndicator:(BOOL)showPageIndicator;
26- (void)shouldDismissKeyboard:(NSString *)dismissKeyboard;
27
28
29@end
30
31@implementation ReactNativePageView {
32 uint16_t _coalescingKey;
33}
34
35- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher {
36 if (self = [super init]) {
37 _scrollEnabled = YES;
38 _pageMargin = 0;
39 _transitionStyle = UIPageViewControllerTransitionStyleScroll;
40 _orientation = UIPageViewControllerNavigationOrientationHorizontal;
41 _currentIndex = 0;
42 _dismissKeyboard = UIScrollViewKeyboardDismissModeNone;
43 _coalescingKey = 0;
44 _eventDispatcher = eventDispatcher;
45 _cachedControllers = [NSHashTable weakObjectsHashTable];
46 _overdrag = YES;
47 }
48 return self;
49}
50
51- (void)layoutSubviews {
52 [super layoutSubviews];
53 if (self.reactPageViewController) {
54 [self shouldScroll:self.scrollEnabled];
55 //Below line fix bug, where the view does not update after orientation changed.
56 [self updateDataSource];
57 }
58}
59
60- (void)didUpdateReactSubviews {
61 if (!self.reactPageViewController && self.reactViewController != nil) {
62 [self embed];
63 [self setupInitialController];
64 } else {
65 [self updateDataSource];
66 }
67}
68
69- (void)didMoveToSuperview {
70 [super didMoveToSuperview];
71 if (!self.reactPageViewController && self.reactViewController != nil) {
72 [self embed];
73 [self setupInitialController];
74 }
75}
76
77- (void)didMoveToWindow {
78 [super didMoveToWindow];
79 if (!self.reactPageViewController && self.reactViewController != nil) {
80 [self embed];
81 [self setupInitialController];
82 }
83}
84
85- (void)embed {
86 NSDictionary *options = @{ UIPageViewControllerOptionInterPageSpacingKey: @(self.pageMargin) };
87 UIPageViewController *pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:self.transitionStyle
88 navigationOrientation:self.orientation
89 options:options];
90 pageViewController.delegate = self;
91 pageViewController.dataSource = self;
92
93 for (UIView *subview in pageViewController.view.subviews) {
94 if([subview isKindOfClass:UIScrollView.class]){
95 ((UIScrollView *)subview).delegate = self;
96 ((UIScrollView *)subview).keyboardDismissMode = _dismissKeyboard;
97 ((UIScrollView *)subview).delaysContentTouches = NO;
98 self.scrollView = (UIScrollView *)subview;
99 }
100 }
101
102 self.reactPageViewController = pageViewController;
103
104 UIPageControl *pageIndicatorView = [self createPageIndicator];
105
106 pageIndicatorView.numberOfPages = self.reactSubviews.count;
107 pageIndicatorView.currentPage = self.initialPage;
108 pageIndicatorView.hidden = !self.showPageIndicator;
109
110 self.reactPageIndicatorView = pageIndicatorView;
111
112 [self reactAddControllerToClosestParent:pageViewController];
113 [pageViewController.view addSubview:pageIndicatorView];
114 [self addSubview:pageViewController.view];
115
116 pageViewController.view.frame = self.bounds;
117
118 [self shouldScroll:self.scrollEnabled];
119
120 if (@available(iOS 9.0, *)) {
121 pageIndicatorView.translatesAutoresizingMaskIntoConstraints = NO;
122 NSLayoutConstraint *bottomConstraint = [pageIndicatorView.bottomAnchor constraintEqualToAnchor: pageViewController.view.bottomAnchor constant:0];
123 NSLayoutConstraint *leadingConstraint = [pageIndicatorView.leadingAnchor constraintEqualToAnchor: pageViewController.view.leadingAnchor constant:0];
124 NSLayoutConstraint *trailingConstraint = [pageIndicatorView.trailingAnchor constraintEqualToAnchor: pageViewController.view.trailingAnchor constant:0];
125
126 [NSLayoutConstraint activateConstraints:@[bottomConstraint, leadingConstraint, trailingConstraint]];
127 }
128 [pageViewController.view layoutIfNeeded];
129}
130
131- (void)shouldScroll:(BOOL)scrollEnabled {
132 _scrollEnabled = scrollEnabled;
133 if (self.reactPageViewController.view) {
134 self.scrollView.scrollEnabled = scrollEnabled;
135 }
136}
137
138- (void)shouldDismissKeyboard:(NSString *)dismissKeyboard {
139 _dismissKeyboard = [dismissKeyboard isEqual: @"on-drag"] ?
140 UIScrollViewKeyboardDismissModeOnDrag : UIScrollViewKeyboardDismissModeNone;
141 self.scrollView.keyboardDismissMode = _dismissKeyboard;
142}
143
144- (void)setupInitialController {
145 UIView *initialView = self.reactSubviews[self.initialPage];
146 if (initialView) {
147 UIViewController *initialController = [[UIViewController alloc] initWithView:initialView];
148 [self.cachedControllers addObject:initialController];
149
150 [self setReactViewControllers:self.initialPage
151 with:initialController
152 direction:UIPageViewControllerNavigationDirectionForward
153 animated:YES];
154 }
155}
156
157- (void)setReactViewControllers:(NSInteger)index
158 with:(UIViewController *)controller
159 direction:(UIPageViewControllerNavigationDirection)direction
160 animated:(BOOL)animated {
161 if (self.reactPageViewController == nil) {
162 return;
163 }
164 __weak ReactNativePageView *weakSelf = self;
165 uint16_t coalescingKey = _coalescingKey++;
166
167 [self.reactPageViewController setViewControllers:@[controller]
168 direction:direction
169 animated:animated
170 completion:^(BOOL finished) {
171
172 weakSelf.currentIndex = index;
173 weakSelf.currentView = controller.view;
174
175 if (weakSelf.eventDispatcher) {
176 [weakSelf.eventDispatcher sendEvent:[[RCTOnPageSelected alloc] initWithReactTag:weakSelf.reactTag position:@(index) coalescingKey:coalescingKey]];
177 }
178
179 }];
180}
181
182- (UIViewController *)currentlyDisplayed {
183 return self.reactPageViewController.viewControllers.firstObject;
184}
185
186- (UIViewController *)findCachedControllerForView:(UIView *)view {
187 for (UIViewController *controller in self.cachedControllers) {
188 if (controller.view.reactTag == view.reactTag) {
189 return controller;
190 }
191 }
192 return nil;
193}
194
195- (void)updateDataSource {
196 if (!self.currentView) {
197 return;
198 }
199
200 NSInteger newIndex = [self.reactSubviews indexOfObject:self.currentView];
201
202 if (newIndex == NSNotFound) {
203 // Current view was removed
204 [self goTo:self.currentIndex animated:NO];
205 } else {
206 [self goTo:newIndex animated:NO];
207 }
208}
209
210- (void)goTo:(NSInteger)index animated:(BOOL)animated {
211 NSInteger numberOfPages = self.reactSubviews.count;
212
213 if (numberOfPages == 0 || index < 0) {
214 return;
215 }
216
217 UIPageViewControllerNavigationDirection direction = (index > self.currentIndex) ? UIPageViewControllerNavigationDirectionForward : UIPageViewControllerNavigationDirectionReverse;
218
219 NSInteger indexToDisplay = index < numberOfPages ? index : numberOfPages - 1;
220
221 UIView *viewToDisplay = self.reactSubviews[indexToDisplay];
222 UIViewController *controllerToDisplay = [self findAndCacheControllerForView:viewToDisplay];
223
224 self.reactPageIndicatorView.numberOfPages = numberOfPages;
225 self.reactPageIndicatorView.currentPage = indexToDisplay;
226
227 [self setReactViewControllers:indexToDisplay
228 with:controllerToDisplay
229 direction:direction
230 animated:animated];
231
232}
233
234- (UIViewController *)findAndCacheControllerForView:(UIView *)viewToDisplay {
235 if (!viewToDisplay) { return nil; }
236
237 UIViewController *controllerToDisplay = [self findCachedControllerForView:viewToDisplay];
238 UIViewController *current = [self currentlyDisplayed];
239
240 if (!controllerToDisplay && current.view.reactTag == viewToDisplay.reactTag) {
241 controllerToDisplay = current;
242 }
243 if (!controllerToDisplay) {
244 controllerToDisplay = [[UIViewController alloc] initWithView:viewToDisplay];
245 }
246 [self.cachedControllers addObject:controllerToDisplay];
247
248 return controllerToDisplay;
249}
250
251- (UIViewController *)nextControllerForController:(UIViewController *)controller
252 inDirection:(UIPageViewControllerNavigationDirection)direction {
253 NSUInteger numberOfPages = self.reactSubviews.count;
254 NSInteger index = [self.reactSubviews indexOfObject:controller.view];
255
256 if (index == NSNotFound) {
257 return nil;
258 }
259
260 direction == UIPageViewControllerNavigationDirectionForward ? index++ : index--;
261
262 if (index < 0 || (index > (numberOfPages - 1))) {
263 return nil;
264 }
265
266 UIView *viewToDisplay = self.reactSubviews[index];
267
268 return [self findAndCacheControllerForView:viewToDisplay];
269}
270
271#pragma mark - UIPageViewControllerDelegate
272
273- (void)pageViewController:(UIPageViewController *)pageViewController
274 didFinishAnimating:(BOOL)finished
275 previousViewControllers:(nonnull NSArray<UIViewController *> *)previousViewControllers
276 transitionCompleted:(BOOL)completed {
277
278 if (completed) {
279 UIViewController* currentVC = [self currentlyDisplayed];
280 NSUInteger currentIndex = [self.reactSubviews indexOfObject:currentVC.view];
281
282 self.currentIndex = currentIndex;
283
284 self.currentView = currentVC.view;
285 self.reactPageIndicatorView.currentPage = currentIndex;
286
287 [self.eventDispatcher sendEvent:[[RCTOnPageSelected alloc] initWithReactTag:self.reactTag position:@(currentIndex) coalescingKey:_coalescingKey++]];
288 [self.eventDispatcher sendEvent:[[RCTOnPageScrollEvent alloc] initWithReactTag:self.reactTag position:@(currentIndex) offset:@(0.0)]];
289 }
290}
291
292#pragma mark - UIPageViewControllerDataSource
293
294- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
295 viewControllerAfterViewController:(UIViewController *)viewController {
296 return [self nextControllerForController:viewController inDirection:UIPageViewControllerNavigationDirectionForward];
297}
298
299- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
300 viewControllerBeforeViewController:(UIViewController *)viewController {
301 return [self nextControllerForController:viewController inDirection:UIPageViewControllerNavigationDirectionReverse];
302}
303
304#pragma mark - UIPageControlDelegate
305
306- (void)shouldShowPageIndicator:(BOOL)showPageIndicator {
307 _showPageIndicator = showPageIndicator;
308
309 if (self.reactPageIndicatorView) {
310 self.reactPageIndicatorView.hidden = !showPageIndicator;
311 }
312}
313
314- (UIPageControl *)createPageIndicator {
315 UIPageControl *pageControl = [[UIPageControl alloc] init];
316 pageControl.tintColor = UIColor.blackColor;
317 pageControl.pageIndicatorTintColor = UIColor.whiteColor;
318 pageControl.currentPageIndicatorTintColor = UIColor.blackColor;
319 [pageControl addTarget:self
320 action:@selector(pageControlValueChanged:)
321 forControlEvents:UIControlEventValueChanged];
322
323 return pageControl;
324}
325
326- (void)pageControlValueChanged:(UIPageControl *)sender {
327 if (sender.currentPage != self.currentIndex) {
328 [self goTo:sender.currentPage animated:YES];
329 }
330}
331
332#pragma mark - UIScrollViewDelegate
333
334- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
335 [self.eventDispatcher sendEvent:[[RCTOnPageScrollStateChanged alloc] initWithReactTag:self.reactTag state:@"dragging" coalescingKey:_coalescingKey++]];
336}
337
338- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
339 [self.eventDispatcher sendEvent:[[RCTOnPageScrollStateChanged alloc] initWithReactTag:self.reactTag state:@"settling" coalescingKey:_coalescingKey++]];
340
341 if (!_overdrag) {
342 if (_currentIndex == 0 && scrollView.contentOffset.x <= scrollView.bounds.size.width) {
343 *targetContentOffset = CGPointMake(scrollView.bounds.size.width, 0);
344 } else if (_currentIndex == _reactPageIndicatorView.numberOfPages -1 && scrollView.contentOffset.x >= scrollView.bounds.size.width) {
345 *targetContentOffset = CGPointMake(scrollView.bounds.size.width, 0);
346 }
347 }
348}
349
350- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
351 [self.eventDispatcher sendEvent:[[RCTOnPageScrollStateChanged alloc] initWithReactTag:self.reactTag state:@"idle" coalescingKey:_coalescingKey++]];
352}
353
354- (BOOL)isHorizontal {
355 return self.orientation == UIPageViewControllerNavigationOrientationHorizontal;
356}
357
358- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
359 CGPoint point = scrollView.contentOffset;
360
361 float offset = 0;
362
363 if (!_overdrag) {
364 if (_currentIndex == 0 && scrollView.contentOffset.x < scrollView.bounds.size.width) {
365 scrollView.contentOffset = CGPointMake(scrollView.bounds.size.width, 0);
366 } else if (_currentIndex == _reactPageIndicatorView.numberOfPages - 1 && scrollView.contentOffset.x > scrollView.bounds.size.width) {
367 scrollView.contentOffset = CGPointMake(scrollView.bounds.size.width, 0);
368 }
369 }
370
371 if (self.isHorizontal) {
372 if (self.frame.size.width != 0) {
373 offset = (point.x - self.frame.size.width)/self.frame.size.width;
374 }
375 } else {
376 if (self.frame.size.height != 0) {
377 offset = (point.y - self.frame.size.height)/self.frame.size.height;
378 }
379 }
380
381 float absoluteOffset = fabs(offset);
382 if(absoluteOffset > 1) {
383 absoluteOffset = 1.0;
384 }
385
386 NSString *scrollDirection = [self determineScrollDirection:scrollView];
387 NSString *oppositeDirection = self.isHorizontal ? @"left" : @"up";
388 NSInteger position = self.currentIndex;
389
390 if(absoluteOffset > 0) {
391 position = [scrollDirection isEqual: oppositeDirection] ? self.currentIndex - 1 : self.currentIndex;
392 absoluteOffset = [scrollDirection isEqual: oppositeDirection] ? 1 - absoluteOffset : absoluteOffset;
393 }
394
395
396 self.lastContentOffset = scrollView.contentOffset;
397 [self.eventDispatcher sendEvent:[[RCTOnPageScrollEvent alloc] initWithReactTag:self.reactTag position:@(position) offset:@(absoluteOffset)]];
398}
399
400- (NSString *)determineScrollDirection:(UIScrollView *)scrollView {
401 NSString *scrollDirection;
402 if (self.isHorizontal) {
403 if (self.lastContentOffset.x > scrollView.contentOffset.x) {
404 scrollDirection = @"left";
405 } else if (self.lastContentOffset.x < scrollView.contentOffset.x) {
406 scrollDirection = @"right";
407 }
408 } else {
409 if (self.lastContentOffset.y > scrollView.contentOffset.y) {
410 scrollDirection = @"up";
411 } else if (self.lastContentOffset.y < scrollView.contentOffset.y) {
412 scrollDirection = @"down";
413 }
414 }
415 return scrollDirection;
416}
417@end