diff --git a/Libraries/Components/Navigation/NavigatorIOS.ios.js b/Libraries/Components/Navigation/NavigatorIOS.ios.js index 226de91f14d..ad653000833 100644 --- a/Libraries/Components/Navigation/NavigatorIOS.ios.js +++ b/Libraries/Components/Navigation/NavigatorIOS.ios.js @@ -308,6 +308,15 @@ var NavigatorIOS = React.createClass({ */ translucent: PropTypes.bool, + /** + * A Boolean value that indicates whether the interactive pop gesture is enabled. Useful + * for enabling/disabling the back swipe navigation gesture. If this prop is not provided, + * the default behavior is for the back swipe gesture to be enabled when the navigation bar + * is shown and disabled when the navigation bar is hidden. Once you've provided + * the interactivePopGestureEnabled prop, you can never restore the default behavior. + */ + interactivePopGestureEnabled: PropTypes.bool, + }, navigator: (undefined: ?Object), @@ -686,7 +695,8 @@ var NavigatorIOS = React.createClass({ style={styles.transitioner} vertical={this.props.vertical} requestedTopOfStack={this.state.requestedTopOfStack} - onNavigationComplete={this._handleNavigationComplete}> + onNavigationComplete={this._handleNavigationComplete} + interactivePopGestureEnabled={this.props.interactivePopGestureEnabled}> {items} diff --git a/React/Views/RCTNavigator.h b/React/Views/RCTNavigator.h index a1905a87d21..57b74499306 100644 --- a/React/Views/RCTNavigator.h +++ b/React/Views/RCTNavigator.h @@ -17,6 +17,7 @@ @property (nonatomic, strong) UIView *reactNavSuperviewLink; @property (nonatomic, assign) NSInteger requestedTopOfStack; +@property (nonatomic, assign) BOOL interactivePopGestureEnabled; - (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m index 03b4a79b7c8..d95089ee461 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -27,6 +27,20 @@ typedef NS_ENUM(NSUInteger, RCTNavigationLock) { 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; @@ -191,13 +205,15 @@ NSInteger kNeverProgressed = -10000; @end -@interface RCTNavigator() +@interface RCTNavigator() @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 *previousViews; @@ -332,8 +348,21 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) } } +- (void)setInteractivePopGestureEnabled:(BOOL)interactivePopGestureEnabled +{ + _interactivePopGestureEnabled = interactivePopGestureEnabled; + + _navigationController.interactivePopGestureRecognizer.delegate = self; + _navigationController.interactivePopGestureRecognizer.enabled = interactivePopGestureEnabled; + + _popGestureState = interactivePopGestureEnabled ? RCTPopGestureStateEnabled : RCTPopGestureStateDisabled; +} + - (void)dealloc { + if (_navigationController.interactivePopGestureRecognizer.delegate == self) { + _navigationController.interactivePopGestureRecognizer.delegate = nil; + } _navigationController.delegate = nil; [_navigationController removeFromParentViewController]; } @@ -343,6 +372,11 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) return _navigationController; } +- (BOOL)gestureRecognizerShouldBegin:(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 @@ -399,7 +433,11 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)freeLock { _navigationController.navigationLock = RCTNavigationLockNone; - _navigationController.interactivePopGestureRecognizer.enabled = YES; + + // 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 + _navigationController.interactivePopGestureRecognizer.enabled = self.popGestureState != RCTPopGestureStateDisabled; } /** diff --git a/React/Views/RCTNavigatorManager.m b/React/Views/RCTNavigatorManager.m index 7356fee0112..3b42facb947 100644 --- a/React/Views/RCTNavigatorManager.m +++ b/React/Views/RCTNavigatorManager.m @@ -27,6 +27,7 @@ RCT_EXPORT_MODULE() RCT_EXPORT_VIEW_PROPERTY(requestedTopOfStack, NSInteger) RCT_EXPORT_VIEW_PROPERTY(onNavigationProgress, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onNavigationComplete, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(interactivePopGestureEnabled, BOOL) // TODO: remove error callbacks RCT_EXPORT_METHOD(requestSchedulingJavaScriptNavigation:(nonnull NSNumber *)reactTag