RCTNavigator.m
24.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "RCTNavigator.h"
#import "RCTAssert.h"
#import "RCTBridge.h"
#import "RCTConvert.h"
#import "RCTEventDispatcher.h"
#import "RCTLog.h"
#import "RCTNavItem.h"
#import "RCTScrollView.h"
#import "RCTUtils.h"
#import "RCTView.h"
#import "RCTWrapperViewController.h"
#import "UIView+React.h"
typedef NS_ENUM(NSUInteger, RCTNavigationLock) {
RCTNavigationLockNone,
RCTNavigationLockNative,
RCTNavigationLockJavaScript
};
// By default the interactive pop gesture will be enabled when the navigation bar is displayed
// and disabled when hidden
// RCTPopGestureStateDefault maps to the default behavior (mentioned above). Once popGestureState
// leaves this value, it can never be returned back to it. This is because, due to a limitation in
// the iOS APIs, once we override the default behavior of the gesture recognizer, we cannot return
// back to it.
// RCTPopGestureStateEnabled will enable the gesture independent of nav bar visibility
// RCTPopGestureStateDisabled will disable the gesture independent of nav bar visibility
typedef NS_ENUM(NSUInteger, RCTPopGestureState) {
RCTPopGestureStateDefault = 0,
RCTPopGestureStateEnabled,
RCTPopGestureStateDisabled
};
NSInteger kNeverRequested = -1;
NSInteger kNeverProgressed = -10000;
@interface UINavigationController ()
// need to declare this since `UINavigationController` doesn't publicly declare the fact that it implements
// UINavigationBarDelegate :(
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item;
@end
// http://stackoverflow.com/questions/5115135/uinavigationcontroller-how-to-cancel-the-back-button-event
// There's no other way to do this unfortunately :(
@interface RCTNavigationController : UINavigationController <UINavigationBarDelegate>
{
dispatch_block_t _scrollCallback;
}
@property (nonatomic, assign) RCTNavigationLock navigationLock;
@end
/**
* In general, `RCTNavigator` examines `_currentViews` (which are React child
* views), and compares them to `_navigationController.viewControllers` (which
* are controlled by UIKit).
*
* It is possible for JavaScript (`_currentViews`) to "get ahead" of native
* (`navigationController.viewControllers`) and vice versa. JavaScript gets
* ahead by adding/removing React subviews. Native gets ahead by swiping back,
* or tapping the back button. In both cases, the other system is initially
* unaware. And in both cases, `RCTNavigator` helps the other side "catch up".
*
* If `RCTNavigator` sees the number of React children have changed, it
* pushes/pops accordingly. If `RCTNavigator` sees a `UIKit` driven push/pop, it
* notifies JavaScript that this has happened, and expects that JavaScript will
* eventually render more children to match `UIKit`. There's no rush for
* JavaScript to catch up. But if it does render anything, it must catch up to
* UIKit. It cannot deviate.
*
* To implement this, we need a lock, which we store on the native thread. This
* lock allows one of the systems to push/pop views. Whoever wishes to
* "get ahead" must obtain the lock. Whoever wishes to "catch up" must obtain
* the lock. One thread may not "get ahead" or "catch up" when the other has
* the lock. Once a thread has the lock, it can only do the following:
*
* 1. If it is behind, it may only catch up.
* 2. If it is caught up or ahead, it may push or pop.
*
*
* ========= Acquiring The Lock ==========
*
* JavaScript asynchronously acquires the lock using a native hook. It might be
* rejected and receive the return value `false`.
*
* We acquire the native lock in `shouldPopItem`, which is called right before
* native tries to push/pop, but only if JavaScript doesn't already have the
* lock.
*
* ======== While JavaScript Has Lock ====
*
* When JavaScript has the lock, we have to block all `UIKit` driven pops:
*
* 1. Block back button navigation:
* - Back button will invoke `shouldPopItem`, from which we return `NO` if
* JavaScript has the lock.
* - Back button will respect the return value `NO` and not permit
* navigation.
*
* 2. Block swipe-to-go-back navigation:
* - Swipe will trigger `shouldPopItem`, but swipe won't respect our `NO`
* return value so we must disable the gesture recognizer while JavaScript
* has the lock.
*
* ======== While Native Has Lock =======
*
* We simply deny JavaScript the right to acquire the lock.
*
*
* ======== Releasing The Lock ===========
*
* Recall that the lock represents who has the right to either push/pop (or
* catch up). As soon as we recognize that the side that has locked has carried
* out what it scheduled to do, we can release the lock, but only after any
* possible animations are completed.
*
* *IF* a scheduled operation results in a push/pop (not all do), then we can
* only release the lock after the push/pop animation is complete because
* UIKit. `didMoveToNavigationController` is invoked when the view is done
* pushing/popping/animating. Native swipe-to-go-back interactions can be
* aborted, however, and you'll never see that method invoked. So just to cover
* that case, we also put an animation complete hook in
* `animateAlongsideTransition` to make sure we free the lock, in case the
* scheduled native push/pop never actually happened.
*
* For JavaScript:
* - When we see that JavaScript has "caught up" to `UIKit`, and no pushes/pops
* were needed, we can release the lock.
* - When we see that JavaScript requires *some* push/pop, it's not yet done
* carrying out what it scheduled to do. Just like with `UIKit` push/pops, we
* still have to wait for it to be done animating
* (`didMoveToNavigationController` is a suitable hook).
*
*/
@implementation RCTNavigationController
/**
* @param callback Callback that is invoked when a "scroll" interaction begins
* so that `RCTNavigator` can notify `JavaScript`.
*/
- (instancetype)initWithScrollCallback:(dispatch_block_t)callback
{
if ((self = [super initWithNibName:nil bundle:nil])) {
_scrollCallback = callback;
}
return self;
}
/**
* Invoked when either a navigation item has been popped off, or when a
* swipe-back gesture has began. The swipe-back gesture doesn't respect the
* return value of this method. The back button does. That's why we have to
* completely disable the gesture recognizer for swipe-back while JS has the
* lock.
*/
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
#if !TARGET_OS_TV
if (self.interactivePopGestureRecognizer.state == UIGestureRecognizerStateBegan) {
if (self.navigationLock == RCTNavigationLockNone) {
self.navigationLock = RCTNavigationLockNative;
if (_scrollCallback) {
_scrollCallback();
}
} else if (self.navigationLock == RCTNavigationLockJavaScript) {
// This should never happen because we disable/enable the gesture
// recognizer when we lock the navigation.
RCTAssert(NO, @"Should never receive gesture start while JS locks navigator");
}
} else
#endif //TARGET_OS_TV
{
if (self.navigationLock == RCTNavigationLockNone) {
// Must be coming from native interaction, lock it - it will be unlocked
// in `didMoveToNavigationController`
self.navigationLock = RCTNavigationLockNative;
if (_scrollCallback) {
_scrollCallback();
}
} else if (self.navigationLock == RCTNavigationLockJavaScript) {
// This should only occur when JS has the lock, and
// - JS is driving the pop
// - Or the back button was pressed
// TODO: We actually want to disable the backbutton while JS has the
// lock, but it's not so easy. Even returning `NO` wont' work because it
// will also block JS driven pops. We simply need to disallow a standard
// back button, and instead use a custom one that tells JS to pop to
// length (`currentReactCount` - 1).
return [super navigationBar:navigationBar shouldPopItem:item];
}
}
return [super navigationBar:navigationBar shouldPopItem:item];
}
@end
@interface RCTNavigator() <RCTWrapperViewControllerNavigationListener, UINavigationControllerDelegate, UIGestureRecognizerDelegate>
@property (nonatomic, copy) RCTDirectEventBlock onNavigationProgress;
@property (nonatomic, copy) RCTBubblingEventBlock onNavigationComplete;
@property (nonatomic, assign) NSInteger previousRequestedTopOfStack;
@property (nonatomic, assign) RCTPopGestureState popGestureState;
// Previous views are only mainted in order to detect incorrect
// addition/removal of views below the `requestedTopOfStack`
@property (nonatomic, copy, readwrite) NSArray<RCTNavItem *> *previousViews;
@property (nonatomic, readwrite, strong) RCTNavigationController *navigationController;
/**
* Display link is used to get high frequency sample rate during
* interaction/animation of view controller push/pop.
*
* - The run loop retains the displayLink.
* - `displayLink` retains its target.
* - We use `invalidate` to remove the `RCTNavigator`'s reference to the
* `displayLink` and remove the `displayLink` from the run loop.
*
*
* `displayLink`:
* --------------
*
* - Even though we could implement the `displayLink` cleanup without the
* `invalidate` hook by adding and removing it from the run loop at the
* right times (begin/end animation), we need to account for the possibility
* that the view itself is destroyed mid-interaction. So we always keep it
* added to the run loop, but start/stop it with interactions/animations. We
* remove it from the run loop when the view will be destroyed by React.
*
* +----------+ +--------------+
* | run loop o----strong--->| displayLink |
* +----------+ +--o-----------+
* | ^
* | |
* strong strong
* | |
* v |
* +---------o---+
* | RCTNavigator |
* +-------------+
*
* `dummyView`:
* ------------
* There's no easy way to get a callback that fires when the position of a
* navigation item changes. The actual layers that are moved around during the
* navigation transition are private. Our only hope is to use
* `animateAlongsideTransition`, to set a dummy view's position to transition
* anywhere from -1.0 to 1.0. We later set up a `CADisplayLink` to poll the
* `presentationLayer` of that dummy view and report the value as a "progress"
* percentage.
*
* It was critical that we added the dummy view as a subview of the
* transitionCoordinator's `containerView`, otherwise the animations would not
* work correctly when reversing the gesture direction etc. This seems to be
* undocumented behavior/requirement.
*
*/
@property (nonatomic, readonly, assign) CGFloat mostRecentProgress;
@property (nonatomic, readonly, strong) NSTimer *runTimer;
@property (nonatomic, readonly, assign) NSInteger currentlyTransitioningFrom;
@property (nonatomic, readonly, assign) NSInteger currentlyTransitioningTo;
// Dummy view that we make animate with the same curve/interaction as the
// navigation animation/interaction.
@property (nonatomic, readonly, strong) UIView *dummyView;
@end
@implementation RCTNavigator
{
__weak RCTBridge *_bridge;
NSInteger _numberOfViewControllerMovesToIgnore;
}
@synthesize paused = _paused;
@synthesize pauseCallback = _pauseCallback;
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
RCTAssertParam(bridge);
if ((self = [super initWithFrame:CGRectZero])) {
_paused = YES;
_bridge = bridge;
_mostRecentProgress = kNeverProgressed;
_dummyView = [[UIView alloc] initWithFrame:CGRectZero];
_previousRequestedTopOfStack = kNeverRequested; // So that we initialize with a push.
_previousViews = @[];
__weak RCTNavigator *weakSelf = self;
_navigationController = [[RCTNavigationController alloc] initWithScrollCallback:^{
[weakSelf dispatchFakeScrollEvent];
}];
_navigationController.delegate = self;
RCTAssert([self requestSchedulingJavaScriptNavigation], @"Could not acquire JS navigation lock on init");
[self addSubview:_navigationController.view];
[_navigationController.view addSubview:_dummyView];
}
return self;
}
RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
- (void)didUpdateFrame:(__unused RCTFrameUpdate *)update
{
if (_currentlyTransitioningFrom != _currentlyTransitioningTo) {
UIView *topView = _dummyView;
id presentationLayer = [topView.layer presentationLayer];
CGRect frame = [presentationLayer frame];
CGFloat nextProgress = ABS(frame.origin.x);
// Don't want to spam the bridge, when the user holds their finger still mid-navigation.
if (nextProgress == _mostRecentProgress) {
return;
}
_mostRecentProgress = nextProgress;
if (_onNavigationProgress) {
_onNavigationProgress(@{
@"fromIndex": @(_currentlyTransitioningFrom),
@"toIndex": @(_currentlyTransitioningTo),
@"progress": @(nextProgress),
});
}
}
}
- (void)setPaused:(BOOL)paused
{
if (_paused != paused) {
_paused = paused;
if (_pauseCallback) {
_pauseCallback();
}
}
}
- (void)setInteractivePopGestureEnabled:(BOOL)interactivePopGestureEnabled
{
#if !TARGET_OS_TV
_interactivePopGestureEnabled = interactivePopGestureEnabled;
_navigationController.interactivePopGestureRecognizer.delegate = self;
_navigationController.interactivePopGestureRecognizer.enabled = interactivePopGestureEnabled;
_popGestureState = interactivePopGestureEnabled ? RCTPopGestureStateEnabled : RCTPopGestureStateDisabled;
#endif
}
- (void)dealloc
{
#if !TARGET_OS_TV
if (_navigationController.interactivePopGestureRecognizer.delegate == self) {
_navigationController.interactivePopGestureRecognizer.delegate = nil;
}
#endif
_navigationController.delegate = nil;
[_navigationController removeFromParentViewController];
}
- (UIViewController *)reactViewController
{
return _navigationController;
}
- (BOOL)gestureRecognizerShouldBegin:(__unused UIGestureRecognizer *)gestureRecognizer
{
return _navigationController.viewControllers.count > 1;
}
/**
* See documentation about lock lifecycle. This is only here to clean up
* swipe-back abort interaction, which leaves us *no* other way to clean up
* locks aside from the animation complete hook.
*/
- (void)navigationController:(UINavigationController *)navigationController
willShowViewController:(__unused UIViewController *)viewController
animated:(__unused BOOL)animated
{
id<UIViewControllerTransitionCoordinator> tc =
navigationController.topViewController.transitionCoordinator;
__weak RCTNavigator *weakSelf = self;
[tc.containerView addSubview: _dummyView];
[tc animateAlongsideTransition: ^(id<UIViewControllerTransitionCoordinatorContext> context) {
RCTWrapperViewController *fromController =
(RCTWrapperViewController *)[context viewControllerForKey:UITransitionContextFromViewControllerKey];
RCTWrapperViewController *toController =
(RCTWrapperViewController *)[context viewControllerForKey:UITransitionContextToViewControllerKey];
// This may be triggered by a navigation controller unrelated to me: if so, ignore.
if (fromController.navigationController != self->_navigationController ||
toController.navigationController != self->_navigationController) {
return;
}
NSUInteger indexOfFrom = [self.reactSubviews indexOfObject:fromController.navItem];
NSUInteger indexOfTo = [self.reactSubviews indexOfObject:toController.navItem];
CGFloat destination = indexOfFrom < indexOfTo ? 1.0 : -1.0;
self->_dummyView.frame = (CGRect){{destination, 0}, CGSizeZero};
self->_currentlyTransitioningFrom = indexOfFrom;
self->_currentlyTransitioningTo = indexOfTo;
self.paused = NO;
}
completion:^(__unused id<UIViewControllerTransitionCoordinatorContext> context) {
[weakSelf freeLock];
self->_currentlyTransitioningFrom = 0;
self->_currentlyTransitioningTo = 0;
self->_dummyView.frame = CGRectZero;
self.paused = YES;
// Reset the parallel position tracker
}];
}
- (BOOL)requestSchedulingJavaScriptNavigation
{
if (_navigationController.navigationLock == RCTNavigationLockNone) {
_navigationController.navigationLock = RCTNavigationLockJavaScript;
#if !TARGET_OS_TV
_navigationController.interactivePopGestureRecognizer.enabled = NO;
#endif
return YES;
}
return NO;
}
- (void)freeLock
{
_navigationController.navigationLock = RCTNavigationLockNone;
// Unless the pop gesture has been explicitly disabled (RCTPopGestureStateDisabled),
// Set interactivePopGestureRecognizer.enabled to YES
// If the popGestureState is RCTPopGestureStateDefault the default behavior will be maintained
#if !TARGET_OS_TV
_navigationController.interactivePopGestureRecognizer.enabled = self.popGestureState != RCTPopGestureStateDisabled;
#endif
}
/**
* A React subview can be inserted/removed at any time, however if the
* `requestedTopOfStack` changes, there had better be enough subviews present
* to satisfy the push/pop.
*/
- (void)insertReactSubview:(RCTNavItem *)view atIndex:(NSInteger)atIndex
{
RCTAssert([view isKindOfClass:[RCTNavItem class]], @"RCTNavigator only accepts RCTNavItem subviews");
RCTAssert(
_navigationController.navigationLock == RCTNavigationLockJavaScript,
@"Cannot change subviews from JS without first locking."
);
[super insertReactSubview:view atIndex:atIndex];
}
- (void)didUpdateReactSubviews
{
// Do nothing, as subviews are managed by `uiManagerDidPerformMounting`
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self reactAddControllerToClosestParent:_navigationController];
_navigationController.view.frame = self.bounds;
}
- (void)removeReactSubview:(RCTNavItem *)subview
{
if (self.reactSubviews.count <= 0 || subview == self.reactSubviews[0]) {
RCTLogError(@"Attempting to remove invalid RCT subview of RCTNavigator");
return;
}
[super removeReactSubview:subview];
}
- (void)handleTopOfStackChanged
{
if (_onNavigationComplete) {
_onNavigationComplete(@{
@"stackLength":@(_navigationController.viewControllers.count)
});
}
}
- (void)dispatchFakeScrollEvent
{
[_bridge.eventDispatcher sendFakeScrollEvent:self.reactTag];
}
/**
* Must be overridden because UIKit removes the view's superview when used
* as a navigator - it's considered outside the view hierarchy.
*/
- (UIView *)reactSuperview
{
RCTAssert(!_bridge.isValid || self.superview != nil, @"put reactNavSuperviewLink back");
UIView *superview = [super reactSuperview];
return superview ?: self.reactNavSuperviewLink;
}
- (void)uiManagerDidPerformMounting
{
// we can't hook up the VC hierarchy in 'init' because the subviews aren't
// hooked up yet, so we do it on demand here
[self reactAddControllerToClosestParent:_navigationController];
NSUInteger viewControllerCount = _navigationController.viewControllers.count;
// The "react count" is the count of views that are visible on the navigation
// stack. There may be more beyond this - that aren't visible, and may be
// deleted/purged soon.
NSUInteger previousReactCount =
_previousRequestedTopOfStack == kNeverRequested ? 0 : _previousRequestedTopOfStack + 1;
NSUInteger currentReactCount = _requestedTopOfStack + 1;
BOOL jsGettingAhead =
// ----- previously caught up ------ ------ no longer caught up -------
viewControllerCount == previousReactCount && currentReactCount != viewControllerCount;
BOOL jsCatchingUp =
// --- previously not caught up ---- --------- now caught up ----------
viewControllerCount != previousReactCount && currentReactCount == viewControllerCount;
BOOL jsMakingNoProgressButNeedsToCatchUp =
// --- previously not caught up ---- ------- still the same -----------
viewControllerCount != previousReactCount && currentReactCount == previousReactCount;
BOOL jsMakingNoProgressAndDoesntNeedTo =
// --- previously caught up -------- ------- still caught up ----------
viewControllerCount == previousReactCount && currentReactCount == previousReactCount;
BOOL jsGettingtooSlow =
// --- previously not caught up -------- ------- no longer caught up ----------
viewControllerCount < previousReactCount && currentReactCount < previousReactCount;
BOOL reactPushOne = jsGettingAhead && currentReactCount == previousReactCount + 1;
BOOL reactPopN = jsGettingAhead && currentReactCount < previousReactCount;
// We can actually recover from this situation, but it would be nice to know
// when this error happens. This simply means that JS hasn't caught up to a
// back navigation before progressing. It's likely a bug in the JS code that
// catches up/schedules navigations.
if (!(jsGettingAhead ||
jsCatchingUp ||
jsMakingNoProgressButNeedsToCatchUp ||
jsMakingNoProgressAndDoesntNeedTo ||
jsGettingtooSlow)) {
RCTLogError(@"JS has only made partial progress to catch up to UIKit");
}
if (currentReactCount > self.reactSubviews.count) {
RCTLogError(@"Cannot adjust current top of stack beyond available views");
}
// Views before the previous React count must not have changed. Views greater than previousReactCount
// up to currentReactCount may have changed.
for (NSUInteger i = 0; i < MIN(self.reactSubviews.count, MIN(_previousViews.count, previousReactCount)); i++) {
if (self.reactSubviews[i] != _previousViews[i]) {
RCTLogError(@"current view should equal previous view");
}
}
if (currentReactCount < 1) {
RCTLogError(@"should be at least one current view");
}
if (jsGettingAhead) {
if (reactPushOne) {
UIView *lastView = self.reactSubviews.lastObject;
RCTWrapperViewController *vc = [[RCTWrapperViewController alloc] initWithNavItem:(RCTNavItem *)lastView];
vc.navigationListener = self;
_numberOfViewControllerMovesToIgnore = 1;
[_navigationController pushViewController:vc animated:(currentReactCount > 1)];
} else if (reactPopN) {
UIViewController *viewControllerToPopTo = _navigationController.viewControllers[(currentReactCount - 1)];
_numberOfViewControllerMovesToIgnore = viewControllerCount - currentReactCount;
[_navigationController popToViewController:viewControllerToPopTo animated:YES];
} else {
RCTLogError(@"Pushing or popping more than one view at a time from JS");
}
} else if (jsCatchingUp) {
[self freeLock]; // Nothing to push/pop
} else {
// Else, JS making no progress, could have been unrelated to anything nav.
return;
}
// Only make a copy of the subviews whose validity we expect to be able to check (in the loop, above),
// otherwise we would unnecessarily retain a reference to view(s) no longer on the React navigation stack:
NSUInteger expectedCount = MIN(currentReactCount, self.reactSubviews.count);
_previousViews = [[self.reactSubviews subarrayWithRange: NSMakeRange(0, expectedCount)] copy];
_previousRequestedTopOfStack = _requestedTopOfStack;
}
// TODO: This will likely fail when performing multiple pushes/pops. We must
// free the lock only after the *last* push/pop.
- (void)wrapperViewController:(RCTWrapperViewController *)wrapperViewController
didMoveToNavigationController:(UINavigationController *)navigationController
{
if (self.superview == nil) {
// If superview is nil, then a JS reload (Cmd+R) happened
// while a push/pop is in progress.
return;
}
RCTAssert(
(navigationController == nil || [_navigationController.viewControllers containsObject:wrapperViewController]),
@"if navigation controller is not nil, it should contain the wrapper view controller"
);
RCTAssert(_navigationController.navigationLock == RCTNavigationLockJavaScript ||
_numberOfViewControllerMovesToIgnore == 0,
@"If JS doesn't have the lock there should never be any pending transitions");
/**
* When JS has the lock we want to keep track of when the request completes
* the pending transition count hitting 0 signifies this, and should always
* remain at 0 when JS does not have the lock
*/
if (_numberOfViewControllerMovesToIgnore > 0) {
_numberOfViewControllerMovesToIgnore -= 1;
}
if (_numberOfViewControllerMovesToIgnore == 0) {
[self handleTopOfStackChanged];
[self freeLock];
}
}
@end