This app provides monitoring and information features for the common freifunk user and the technical stuff of a freifunk community.
Code base is taken from a TUM Practical Course project and added here to see if Freifunk Altdorf can use it.
https://www.freifunk-altdorf.de
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
630 lines
24 KiB
630 lines
24 KiB
/** |
|
* Copyright (c) 2015-present, Facebook, Inc. |
|
* |
|
* This source code is licensed under the MIT license found in the |
|
* LICENSE file in the root directory of this source tree. |
|
*/ |
|
|
|
#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
|
|
|