remove navigator and webview which now build out of RNC
This commit is contained in:
Родитель
92d82859ee
Коммит
e66e9370cc
|
@ -1,635 +0,0 @@
|
|||
/**
|
||||
* 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
|
||||
{
|
||||
UIView *superview = [super reactSuperview];
|
||||
return superview ?: self.reactNavSuperviewLink;
|
||||
}
|
||||
|
||||
- (void)uiManagerDidPerformMounting
|
||||
{
|
||||
// TODO(OC#2710034): This file was removed in the facebook repo.
|
||||
if (self.reactSubviews.count == 0) {
|
||||
// subviews aren't hooked up yet
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
|
@ -1,344 +0,0 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// TODO(macOS ISS#2323203)
|
||||
|
||||
#import "RCTWebView.h"
|
||||
|
||||
#import <WebKit/WebKit.h>
|
||||
|
||||
#import "RCTAutoInsetsProtocol.h"
|
||||
#import "RCTConvert.h"
|
||||
#import "RCTEventDispatcher.h"
|
||||
#import "RCTLog.h"
|
||||
#import "RCTUtils.h"
|
||||
#import "RCTView.h"
|
||||
#import "UIView+React.h"
|
||||
|
||||
NSString *const RCTJSNavigationScheme = @"react-js-navigation";
|
||||
NSString *const RCTJSPostMessageHost = @"postMessage";
|
||||
|
||||
@interface RCTWebView () <WebFrameLoadDelegate, WebResourceLoadDelegate, RCTAutoInsetsProtocol>
|
||||
|
||||
@property (nonatomic, copy) RCTDirectEventBlock onLoadingStart;
|
||||
@property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish;
|
||||
@property (nonatomic, copy) RCTDirectEventBlock onLoadingError;
|
||||
@property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest;
|
||||
@property (nonatomic, copy) RCTDirectEventBlock onMessage;
|
||||
|
||||
@end
|
||||
|
||||
@implementation RCTWebView
|
||||
{
|
||||
WebView *_webView;
|
||||
NSString *_injectedJavaScript;
|
||||
NSString *_pageTitle;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
_webView.frameLoadDelegate = nil;
|
||||
_webView.resourceLoadDelegate = nil;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if ((self = [super initWithFrame:frame])) {
|
||||
CALayer *viewLayer = [CALayer layer];
|
||||
[viewLayer setBackgroundColor:[[NSColor clearColor] CGColor]]; //RGB plus Alpha Channel
|
||||
[self setWantsLayer:YES]; // view's backing store is using a Core Animation Layer
|
||||
[self setLayer:viewLayer];
|
||||
_automaticallyAdjustContentInsets = YES;
|
||||
_webView = [[WebView alloc] initWithFrame:self.bounds];
|
||||
[WebView registerURLSchemeAsLocal:RCTJSNavigationScheme];
|
||||
[_webView setFrameLoadDelegate:self];
|
||||
[_webView setResourceLoadDelegate:self];
|
||||
[self addSubview:_webView];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
|
||||
|
||||
- (void)goForward
|
||||
{
|
||||
[_webView goForward];
|
||||
}
|
||||
|
||||
- (void)goBack
|
||||
{
|
||||
[_webView goBack];
|
||||
}
|
||||
|
||||
- (void)reload
|
||||
{
|
||||
[_webView reload:self];
|
||||
}
|
||||
|
||||
- (void)reactSetFrame:(CGRect)frame
|
||||
{
|
||||
[super reactSetFrame:frame];
|
||||
[_webView setFrame:frame];
|
||||
}
|
||||
|
||||
- (void)stopLoading
|
||||
{
|
||||
[_webView.webFrame stopLoading];
|
||||
}
|
||||
|
||||
- (void)postMessage:(NSString *)message
|
||||
{
|
||||
NSDictionary *eventInitDict = @{
|
||||
@"data": message,
|
||||
};
|
||||
NSString *source = [NSString
|
||||
stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
|
||||
RCTJSONStringify(eventInitDict, NULL)
|
||||
];
|
||||
[_webView stringByEvaluatingJavaScriptFromString:source];
|
||||
}
|
||||
|
||||
- (void)injectJavaScript:(NSString *)script
|
||||
{
|
||||
[_webView stringByEvaluatingJavaScriptFromString:script];
|
||||
}
|
||||
|
||||
- (void)setSource:(NSDictionary *)source
|
||||
{
|
||||
if (![_source isEqualToDictionary:source]) {
|
||||
_source = [source copy];
|
||||
|
||||
// Check for a static html source first
|
||||
NSString *html = [RCTConvert NSString:source[@"html"]];
|
||||
if (html) {
|
||||
NSURL *baseURL = [RCTConvert NSURL:source[@"baseUrl"]];
|
||||
if (!baseURL) {
|
||||
baseURL = [NSURL URLWithString:@"about:blank"];
|
||||
}
|
||||
[_webView.mainFrame loadHTMLString:html baseURL:baseURL];
|
||||
return;
|
||||
}
|
||||
|
||||
NSURLRequest *request = [RCTConvert NSURLRequest:source];
|
||||
// Because of the way React works, as pages redirect, we actually end up
|
||||
// passing the redirect urls back here, so we ignore them if trying to load
|
||||
// the same url. We'll expose a call to 'reload' to allow a user to load
|
||||
// the existing page.
|
||||
if ([request.URL isEqual:_webView.mainFrameURL]) {
|
||||
return;
|
||||
}
|
||||
if (!request.URL) {
|
||||
// Clear the webview
|
||||
[_webView.mainFrame loadHTMLString:@"" baseURL:nil];
|
||||
return;
|
||||
}
|
||||
[_webView.mainFrame loadRequest:request];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)layout
|
||||
{
|
||||
[super layout];
|
||||
_webView.frame = self.bounds;
|
||||
}
|
||||
|
||||
|
||||
- (void)refreshContentInset
|
||||
{
|
||||
// leaving here to avoid warning about missing method in RCTAutoInsetsProtocol
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(NSColor *)backgroundColor
|
||||
{
|
||||
CGFloat alpha = CGColorGetAlpha(backgroundColor.CGColor);
|
||||
[self.layer setOpaque:(alpha == 1.0)];
|
||||
[[_webView layer] setBackgroundColor:[backgroundColor CGColor]];
|
||||
}
|
||||
|
||||
- (NSColor *)backgroundColor
|
||||
{
|
||||
CGColorRef backgroundColor = _webView.layer.backgroundColor;
|
||||
if (backgroundColor) {
|
||||
return [NSColor colorWithCGColor:backgroundColor];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSMutableDictionary<NSString *, id> *)baseEvent
|
||||
{
|
||||
NSMutableDictionary<NSString *, id> *event = [[NSMutableDictionary alloc] initWithDictionary:@{
|
||||
@"url": _webView.mainFrameURL ?: @"",
|
||||
@"loading" : @(_webView.loading),
|
||||
@"title": _pageTitle ?: @"",
|
||||
@"canGoBack": @(_webView.canGoBack),
|
||||
@"canGoForward" : @(_webView.canGoForward),
|
||||
}];
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - WebFrameLoadDelegate methods
|
||||
|
||||
- (void)webView:(WebView *)sender
|
||||
didCommitLoadForFrame:(WebFrame *)frame
|
||||
{
|
||||
if (_onLoadingStart && frame == [sender mainFrame]) {
|
||||
NSString *url = [[[[frame dataSource] request] URL] absoluteString];
|
||||
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
||||
[event addEntriesFromDictionary: @{ @"url": url }];
|
||||
|
||||
// at this point of time the web view is done resolving http->https redirects
|
||||
// and has the final uri that is going to be loaded
|
||||
// lets set it to _source to avoid unnecessary reloads when someone sets state
|
||||
// in onNavigationStateChange callback (like the RNTester WebViewExample does).
|
||||
NSMutableDictionary *s = [[NSMutableDictionary alloc] init];
|
||||
[s addEntriesFromDictionary: _source];
|
||||
[s setObject:url forKey:@"uri" ];
|
||||
_source = s;
|
||||
|
||||
_onLoadingStart(event);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)webView:(WebView *)sender didReceiveTitle:(NSString *)title forFrame:(WebFrame *)frame
|
||||
{
|
||||
// Report feedback only for the main frame.
|
||||
if (frame == [sender mainFrame]){
|
||||
_pageTitle = title;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)webView:(WebView *)sender
|
||||
didFinishLoadForFrame:(WebFrame *)frame {
|
||||
|
||||
// continue only for top level frame
|
||||
if (frame != [sender mainFrame]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_messagingEnabled) {
|
||||
#if RCT_DEV
|
||||
// See isNative in lodash
|
||||
NSString *testPostMessageNative = @"String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage')";
|
||||
BOOL postMessageIsNative = [
|
||||
[_webView stringByEvaluatingJavaScriptFromString:testPostMessageNative]
|
||||
isEqualToString:@"true"
|
||||
];
|
||||
if (!postMessageIsNative) {
|
||||
RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
|
||||
}
|
||||
#endif
|
||||
NSString *source = [NSString stringWithFormat:
|
||||
@"window.originalPostMessage = window.postMessage;"
|
||||
"window.postMessage = function(data) {"
|
||||
"window.location = '%@://%@?' + encodeURIComponent(String(data));"
|
||||
"};", RCTJSNavigationScheme, RCTJSPostMessageHost
|
||||
];
|
||||
[_webView stringByEvaluatingJavaScriptFromString:source];
|
||||
}
|
||||
|
||||
|
||||
if (_injectedJavaScript != nil) {
|
||||
NSString *jsEvaluationValue = [frame.webView stringByEvaluatingJavaScriptFromString:_injectedJavaScript];
|
||||
|
||||
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
||||
event[@"jsEvaluationValue"] = jsEvaluationValue;
|
||||
|
||||
_onLoadingFinish(event);
|
||||
}
|
||||
// we only need the final 'finishLoad' call so only fire the event when we're actually done loading.
|
||||
else if (_onLoadingFinish && ![frame.webView.mainFrameURL isEqualToString:@"about:blank"]) {
|
||||
_onLoadingFinish([self baseEvent]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
- (void)webView:(__unused WebView *)sender
|
||||
didFailLoadWithError:(NSError *)error
|
||||
forFrame:(__unused WebFrame *)frame
|
||||
{
|
||||
if (_onLoadingError) {
|
||||
if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) {
|
||||
// NSURLErrorCancelled is reported when a page has a redirect OR if you load
|
||||
// a new URL in the WebView before the previous one came back. We can just
|
||||
// ignore these since they aren't real errors.
|
||||
// http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
||||
[event addEntriesFromDictionary:@{
|
||||
@"domain": error.domain,
|
||||
@"code": @(error.code),
|
||||
@"description": error.localizedDescription,
|
||||
}];
|
||||
_onLoadingError(event);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)webView:(WebView *)sender
|
||||
willPerformClientRedirectToURL:(NSURL *)URL
|
||||
delay:(NSTimeInterval)seconds
|
||||
fireDate:(NSDate *)date
|
||||
forFrame:(WebFrame *)frame {
|
||||
|
||||
BOOL isJSNavigation = [URL.scheme isEqualToString:RCTJSNavigationScheme];
|
||||
if (isJSNavigation && [URL.host isEqualToString:RCTJSPostMessageHost]) {
|
||||
NSString *data = URL.query;
|
||||
data = [data stringByReplacingOccurrencesOfString:@"+" withString:@" "];
|
||||
data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
||||
[event addEntriesFromDictionary: @{
|
||||
@"data": data,
|
||||
}];
|
||||
_onMessage(event);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#pragma mark - WebResourceLoadDelegate methods
|
||||
- (NSURLRequest *)webView:(WebView *)sender
|
||||
resource:(id)identifier
|
||||
willSendRequest:(NSURLRequest *)request
|
||||
redirectResponse:(NSURLResponse *)redirectResponse
|
||||
fromDataSource:(WebDataSource *)dataSource
|
||||
{
|
||||
BOOL isJSNavigation = [request.URL.scheme isEqualToString:RCTJSNavigationScheme];
|
||||
|
||||
// let the caller decide if they want to continue with the request
|
||||
// skip this for the JS Navigation handler
|
||||
if (!isJSNavigation && _onShouldStartLoadWithRequest) {
|
||||
|
||||
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
||||
[event addEntriesFromDictionary: @{ @"url": (request.URL).absoluteString }];
|
||||
if (![self.delegate webView:self
|
||||
shouldStartLoadForRequest:event
|
||||
withCallback:_onShouldStartLoadWithRequest]) {
|
||||
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
if (isJSNavigation && [request.URL.host isEqualToString:RCTJSPostMessageHost]) {
|
||||
NSString *data = request.URL.query;
|
||||
data = [data stringByReplacingOccurrencesOfString:@"+" withString:@" "];
|
||||
data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
||||
[event addEntriesFromDictionary: @{
|
||||
@"data": data,
|
||||
}];
|
||||
_onMessage(event);
|
||||
}
|
||||
|
||||
// JS Navigation handler
|
||||
return request;
|
||||
}
|
||||
|
||||
@end
|
Загрузка…
Ссылка в новой задаче