600 lines
23 KiB
Objective-C
600 lines
23 KiB
Objective-C
// Copyright 2004-present Facebook. All Rights Reserved.
|
|
|
|
#import "RCTNavigator.h"
|
|
|
|
#import "RCTAssert.h"
|
|
#import "RCTConvert.h"
|
|
#import "RCTEventDispatcher.h"
|
|
#import "RCTLog.h"
|
|
#import "RCTNavItem.h"
|
|
#import "RCTUtils.h"
|
|
#import "RCTView.h"
|
|
#import "RCTWrapperViewController.h"
|
|
#import "UIView+ReactKit.h"
|
|
|
|
typedef NS_ENUM(NSUInteger, RCTNavigationLock) {
|
|
RCTNavigationLockNone,
|
|
RCTNavigationLockNative,
|
|
RCTNavigationLockJavaScript
|
|
};
|
|
|
|
NSInteger kNeverRequested = -1;
|
|
NSInteger kNeverProgressed = -10000;
|
|
|
|
|
|
@interface UINavigationController ()
|
|
|
|
// need to declare this since `UINavigationController` doesnt 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 rener 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
|
|
|
|
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
|
|
{
|
|
RCT_NOT_DESIGNATED_INITIALIZER();
|
|
}
|
|
|
|
- (instancetype)initWithNavigationBarClass:(Class)navigationBarClass toolbarClass:(Class)toolbarClass
|
|
{
|
|
RCT_NOT_DESIGNATED_INITIALIZER();
|
|
}
|
|
|
|
- (instancetype)initWithRootViewController:(UIViewController *)rootViewController
|
|
{
|
|
RCT_NOT_DESIGNATED_INITIALIZER();
|
|
}
|
|
|
|
/**
|
|
* @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 (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 {
|
|
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>
|
|
{
|
|
RCTEventDispatcher *_eventDispatcher;
|
|
NSInteger _numberOfViewControllerMovesToIgnore;
|
|
}
|
|
|
|
@property (nonatomic, assign) NSInteger previousRequestedTopOfStack;
|
|
|
|
// Previous views are only mainted in order to detect incorrect
|
|
// addition/removal of views below the `requestedTopOfStack`
|
|
@property (nonatomic, copy, readwrite) NSArray *previousViews;
|
|
@property (nonatomic, readwrite, strong) NSMutableArray *currentViews;
|
|
@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 `reactWillDestroy` 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
|
|
* `reactWillDestroy` 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, readwrite, strong) CADisplayLink *displayLink;
|
|
@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
|
|
|
|
- (instancetype)initWithFrame:(CGRect)frame
|
|
{
|
|
RCT_NOT_DESIGNATED_INITIALIZER();
|
|
}
|
|
|
|
- (id)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
|
|
{
|
|
if ((self = [super initWithFrame:CGRectZero])) {
|
|
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(reportNavigationProgress:)];
|
|
_mostRecentProgress = kNeverProgressed;
|
|
_dummyView = [[UIView alloc] initWithFrame:CGRectZero];
|
|
if (_displayLink) {
|
|
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
|
|
_displayLink.paused = YES;
|
|
} else {
|
|
// It's okay to leak this on a build bot.
|
|
RCTLogWarn(@"Failed to create a display link (probably on automated build system) - using an NSTimer for AppEngine instead.");
|
|
_runTimer = [NSTimer scheduledTimerWithTimeInterval:(1.0 / 60.0) target:self selector:@selector(reportNavigationProgress:) userInfo:nil repeats:YES];
|
|
}
|
|
_eventDispatcher = eventDispatcher;
|
|
_previousRequestedTopOfStack = kNeverRequested; // So that we initialize with a push.
|
|
_previousViews = @[];
|
|
_currentViews = [[NSMutableArray alloc] initWithCapacity:0];
|
|
__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;
|
|
}
|
|
|
|
- (void)reportNavigationProgress:(CADisplayLink *)sender
|
|
{
|
|
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;
|
|
[_eventDispatcher sendEventWithName:@"topNavigationProgress" body:@{
|
|
@"fromIndex": @(_currentlyTransitioningFrom),
|
|
@"toIndex": @(_currentlyTransitioningTo),
|
|
@"progress": @(nextProgress),
|
|
@"target": self.reactTag
|
|
}];
|
|
}
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
_navigationController.delegate = nil;
|
|
}
|
|
|
|
- (UIViewController *)backingViewController
|
|
{
|
|
return _navigationController;
|
|
}
|
|
|
|
/**
|
|
* 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:(UIViewController *)viewController
|
|
animated:(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];
|
|
NSUInteger indexOfFrom = [_currentViews indexOfObject:fromController.navItem];
|
|
NSUInteger indexOfTo = [_currentViews indexOfObject:toController.navItem];
|
|
CGFloat destination = indexOfFrom < indexOfTo ? 1.0f : -1.0f;
|
|
_dummyView.frame = CGRectMake(destination, 0.0f, 0.0f, 0.0f);
|
|
_currentlyTransitioningFrom = indexOfFrom;
|
|
_currentlyTransitioningTo = indexOfTo;
|
|
if (indexOfFrom != indexOfTo) {
|
|
_displayLink.paused = NO;
|
|
}
|
|
}
|
|
completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
|
[weakSelf freeLock];
|
|
_currentlyTransitioningFrom = 0;
|
|
_currentlyTransitioningTo = 0;
|
|
_dummyView.frame = CGRectMake(0.0f, 0.0f, 0.0f, 0.0f);
|
|
_displayLink.paused = YES;
|
|
// Reset the parallel position tracker
|
|
}];
|
|
}
|
|
|
|
- (BOOL)requestSchedulingJavaScriptNavigation
|
|
{
|
|
if (_navigationController.navigationLock == RCTNavigationLockNone) {
|
|
_navigationController.navigationLock = RCTNavigationLockJavaScript;
|
|
_navigationController.interactivePopGestureRecognizer.enabled = NO;
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (void)freeLock
|
|
{
|
|
_navigationController.navigationLock = RCTNavigationLockNone;
|
|
_navigationController.interactivePopGestureRecognizer.enabled = YES;
|
|
}
|
|
|
|
/**
|
|
* 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:(UIView *)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."
|
|
);
|
|
[_currentViews insertObject:view atIndex:atIndex];
|
|
}
|
|
|
|
- (NSArray *)reactSubviews
|
|
{
|
|
return _currentViews;
|
|
}
|
|
|
|
- (BOOL)isValid
|
|
{
|
|
return _displayLink != nil;
|
|
}
|
|
|
|
- (void)invalidate
|
|
{
|
|
// Prevent displayLink from retaining the navigator indefinitely
|
|
[_displayLink invalidate];
|
|
_displayLink = nil;
|
|
_runTimer = nil;
|
|
}
|
|
|
|
- (void)layoutSubviews
|
|
{
|
|
[super layoutSubviews];
|
|
_navigationController.view.frame = self.bounds;
|
|
}
|
|
|
|
- (void)removeReactSubview:(UIView *)subview
|
|
{
|
|
if (_currentViews.count <= 0 || subview == _currentViews[0]) {
|
|
RCTLogError(@"Attempting to remove invalid RCT subview of RCTNavigator");
|
|
return;
|
|
}
|
|
[_currentViews removeObject:subview];
|
|
}
|
|
|
|
- (void)handleTopOfStackChanged
|
|
{
|
|
[_eventDispatcher sendEventWithName:@"topNavigateBack" body:@{
|
|
@"target":self.reactTag,
|
|
@"stackLength":@(_navigationController.viewControllers.count)
|
|
}];
|
|
}
|
|
|
|
- (void)dispatchFakeScrollEvent
|
|
{
|
|
[_eventDispatcher sendScrollEventWithType:RCTScrollEventTypeMove
|
|
reactTag:self.reactTag
|
|
scrollView:nil
|
|
userData:nil];
|
|
}
|
|
|
|
/**
|
|
* Must be overridden because UIKit destroys the views superview link when used
|
|
* as a navigator - it's considered outside the view hierarchy.
|
|
*/
|
|
- (UIView *)reactSuperview
|
|
{
|
|
RCTAssert(self.superview != nil, @"put reactNavSuperviewLink back");
|
|
return self.superview ? self.superview : self.reactNavSuperviewLink;
|
|
}
|
|
|
|
- (void)addControllerToClosestParent:(UIViewController *)controller
|
|
{
|
|
if (!controller.parentViewController) {
|
|
id responder = [self.superview nextResponder];
|
|
while (responder && ![responder isKindOfClass:[UIViewController class]]) {
|
|
responder = [responder nextResponder];
|
|
}
|
|
if (responder) {
|
|
[responder addChildViewController:controller];
|
|
[controller didMoveToParentViewController:responder];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)reactBridgeDidFinishTransaction
|
|
{
|
|
// 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 addControllerToClosestParent:_navigationController];
|
|
|
|
NSInteger 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.
|
|
NSInteger previousReactCount =
|
|
_previousRequestedTopOfStack == kNeverRequested ? 0 : _previousRequestedTopOfStack + 1;
|
|
NSInteger 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 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. Eventually, let's recover from this
|
|
// error state, but in the mean time, let's get notified about any JS bugs.
|
|
RCTAssert(
|
|
jsGettingAhead ||
|
|
jsCatchingUp ||
|
|
jsMakingNoProgressButNeedsToCatchUp ||
|
|
jsMakingNoProgressAndDoesntNeedTo,
|
|
@"JS has only made partial progress to catch up to UIKit"
|
|
);
|
|
NSAssert(
|
|
currentReactCount <= _currentViews.count,
|
|
@"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 (NSInteger i = 0; i < MIN(_currentViews.count, MIN(_previousViews.count, previousReactCount)); i++) {
|
|
NSAssert(_currentViews[i] == _previousViews[i], @"current view should equal previous view");
|
|
}
|
|
RCTAssert(currentReactCount >= 1, @"should be at least one current view");
|
|
if (jsGettingAhead) {
|
|
if (reactPushOne) {
|
|
UIView *lastView = [_currentViews lastObject];
|
|
RCTWrapperViewController *vc = [[RCTWrapperViewController alloc] initWithNavItem:(RCTNavItem *)lastView eventDispatcher:_eventDispatcher];
|
|
vc.navigationListener = self;
|
|
_numberOfViewControllerMovesToIgnore = 1;
|
|
[_navigationController pushViewController:vc animated:(currentReactCount > 1)];
|
|
} else if (reactPopN) {
|
|
UIViewController *viewControllerToPopTo = [[_navigationController viewControllers] objectAtIndex:(currentReactCount - 1)];
|
|
_numberOfViewControllerMovesToIgnore = viewControllerCount - currentReactCount;
|
|
[_navigationController popToViewController:viewControllerToPopTo animated:YES];
|
|
} else {
|
|
RCTAssert(NO, @"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;
|
|
}
|
|
|
|
_previousViews = [_currentViews 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 container 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
|