From 4d2c72b977db0c837df0ac0b7de5b8b71eeafffb Mon Sep 17 00:00:00 2001 From: Adam Comella Date: Fri, 6 May 2016 03:17:28 -0700 Subject: [PATCH] NavigatorIOS: Expose interactivePopGestureEnabled property Summary: Previously, the back swipe navigation gesture would be enabled when the navigation bar is shown and disabled when the navigation bar is hidden. This change enables developers to control the back swipe gesture independently of the visibility of the navigation bar. An example use case would be that an app wants to render a custom navigation bar so it sets `navigationBarHidden` to true and it wants to enable the back swipe gesture so it sets `interactivePopGestureEnabled` to true. **Test plan (required)** - Created a test app to verify setting `interactivePopGestureEnabled` to `true` and `false` with the navigation bar both hidden and shown. - Verified prop works in a larger app. Adam Comella Microsoft Corp. Closes https://github.com/facebook/react-native/pull/7369 Differential Revision: D3269304 Pulled By: javache fb-gh-sync-id: ec4324f6517cec4b4fc4f62c4394dc9208a8af6a fbshipit-source-id: ec4324f6517cec4b4fc4f62c4394dc9208a8af6a --- .../Components/Navigation/NavigatorIOS.ios.js | 12 +++++- React/Views/RCTNavigator.h | 1 + React/Views/RCTNavigator.m | 42 ++++++++++++++++++- React/Views/RCTNavigatorManager.m | 1 + 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/Libraries/Components/Navigation/NavigatorIOS.ios.js b/Libraries/Components/Navigation/NavigatorIOS.ios.js index 226de91f1..ad6530008 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 a1905a87d..57b744993 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 03b4a79b7..d95089ee4 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 7356fee01..3b42facb9 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