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
|