react-native-collapsible-tab-view icon indicating copy to clipboard operation
react-native-collapsible-tab-view copied to clipboard

Release gesture (activeOffset?) when on first tab to enable navigator swipe/pop

Open hirbod opened this issue 3 years ago • 15 comments

Feature request

Currently, the TabView is blocking navigator gestures, eg you cant swipe to go back/pop. It would be nice to release the gesture (guess you're able to do this with activeOffsetX on gesture handler) to enable navigator swipe. This should only happen when on first tab and the swipe direction is correct.

Current behavior

Its blocking. Currently only working when swiping on the tabs or anything that blocks pointer events.

hirbod avatar Mar 26 '22 14:03 hirbod

Looks like there is no gesture handler, its just a ScrollView. I tried to make it work but couldn't unfortunately. Not sure how to achieve this, but its kind sad hat swipe to go back is not working.

hirbod avatar Mar 28 '22 03:03 hirbod

There's a workaround here that may work:

https://github.com/react-navigation/react-navigation/issues/7878#issuecomment-1035927935

Use it like <Tabs.ScrollView hitSlop={{ left: -10 }}

Unfortunately it will also prevent any taps within that specific area of the scroll view, so tapping buttons there won't work. Try playing with the value, something like -10 should work decently.

andreialecu avatar Mar 28 '22 20:03 andreialecu

I will try. I am using react-native-screens native-stack and the option fullScreenGesture, so its actually a bummer but I will try now. I am using FlatList. I will report back if this workaround is working. Hacking around yesterday, the only way I was able to "somehow" make it work (super buggy), was to enable "bounces" and check the x-value inside the animatedScroll event handler and setNativeProps to the container ref scrollEnabled: false, when x is < 0. But it did not work out very well.

hirbod avatar Mar 28 '22 20:03 hirbod

@andreialecu unfortunately, your workaround is neither working on createNativeStackNavigator nor on createStackNavigator. I see the hitSlop is preventing tabs 50px from left (buttons not working), but it does not help with the gesture issue.

hirbod avatar Mar 28 '22 20:03 hirbod

@hirbod try doing it like this:

<Tabs.Container pagerProps={{ hitSlop: { left: -50 }}} />

cloudorbush avatar Mar 28 '22 20:03 cloudorbush

Ok, the only way to make it work was patching Container.js with patch-package and add hitslop: -10 there @andreialecu.

Edit: Oh, you have been faster + I didn't see the pagerProps prop. So no patch-package required. Not the "best" solution but definitely one I can live with!

hirbod avatar Mar 28 '22 20:03 hirbod

Yupp, that is working @10000multiplier :). Thanks a ton to both of you guys. Value of -10 to -15 is enough and won't break my UI

hirbod avatar Mar 28 '22 20:03 hirbod

This is broken with the current RC (version 5)

hirbod avatar May 06 '22 09:05 hirbod

I'm done with workarounds. Patch-Package for the win (works with fullScreenGesture by RNS and the default swipe back gesture)

I just added both patches by @intergalacticspacehighway and also updated them for more recent versions, working great for me for now.

diff --git a/node_modules/react-native-pager-view/ios/ReactNativePageView.m b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
index 8f20bf8..9ec716f 100644
--- a/node_modules/react-native-pager-view/ios/ReactNativePageView.m
+++ b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
@@ -89,6 +89,9 @@ - (void)didMoveToWindow {
         [self embed];
         [self setupInitialController];
     }
+    if (self.reactViewController.navigationController != nil && self.reactViewController.navigationController.interactivePopGestureRecognizer != nil) {
+        [self.scrollView.panGestureRecognizer requireGestureRecognizerToFail:self.reactViewController.navigationController.interactivePopGestureRecognizer];
+    } 
 }
 
 - (void)embed {
diff --git a/node_modules/react-native-screens/ios/RNSScreenStack.m b/node_modules/react-native-screens/ios/RNSScreenStack.m
index 47c8f8d..d261d0e 100644
--- a/node_modules/react-native-screens/ios/RNSScreenStack.m
+++ b/node_modules/react-native-screens/ios/RNSScreenStack.m
@@ -10,6 +10,7 @@
 #import <React/RCTTouchHandler.h>
 #import <React/RCTUIManager.h>
 #import <React/RCTUIManagerUtils.h>
+#import "ReactNativePageView.h"
 
 @interface RNSScreenStackView () <
     UINavigationControllerDelegate,
@@ -594,6 +595,10 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
     return NO;
   }
 
+  if ([gestureRecognizer isKindOfClass:[_controller.interactivePopGestureRecognizer class]]) {
+    return YES;
+  }
+
 #if TARGET_OS_TV
   [self cancelTouchesInParent];
   return YES;
@@ -637,6 +642,20 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
 #endif
 }
 
+- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
+    if ([otherGestureRecognizer isKindOfClass: NSClassFromString(@"UIScrollViewPanGestureRecognizer")] && [otherGestureRecognizer.view.reactViewController isKindOfClass: [UIPageViewController class]]) {
+      UIPageViewController* pageController = otherGestureRecognizer.view.reactViewController;
+      if (pageController != nil && [pageController.delegate isKindOfClass:[ReactNativePageView class]]) {
+        ReactNativePageView* page = pageController.delegate;
+        if (page != nil && page.currentIndex == 0) {
+          return YES;
+        }
+      }
+    }
+    return NO;
+}
+
+
 #if !TARGET_OS_TV
 - (void)setupGestureHandlers
 {

All credits goes to @intergalacticspacehighway

hirbod avatar May 06 '22 10:05 hirbod

@hirbod I cannot for the life of me get this working (including adding patches, upgrading versions, adding hitslop, etc.). Is this still working for you? Do you happen to have an example code using the Tabs.Container and stuff?

This issue is driving me crazy!

mikefogg avatar Nov 30 '22 16:11 mikefogg

I have a working patch for pager-view v5.4.25 (does only work the native-stack though, not stack) which doesn't need patches to react-native-screens.

diff --git a/node_modules/react-native-pager-view/ios/ReactNativePageView.m b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
index eacfbe8..2477039 100644
--- a/node_modules/react-native-pager-view/ios/ReactNativePageView.m
+++ b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
@@ -9,7 +9,7 @@
 #import "RCTOnPageSelected.h"
 #import <math.h>
 
-@interface ReactNativePageView () <UIPageViewControllerDataSource, UIPageViewControllerDelegate, UIScrollViewDelegate>
+@interface ReactNativePageView () <UIPageViewControllerDataSource, UIPageViewControllerDelegate, UIScrollViewDelegate, UIGestureRecognizerDelegate>
 
 @property(nonatomic, strong) UIPageViewController *reactPageViewController;
 @property(nonatomic, strong) UIPageControl *reactPageIndicatorView;
@@ -82,6 +82,11 @@ - (void)didMoveToWindow {
         [self setupInitialController];
     }
 
+    UIPanGestureRecognizer* gesture = [UIPanGestureRecognizer new];
+
+    gesture.delegate = self;
+    [self addGestureRecognizer: gesture];
+ 
     if (self.reactViewController.navigationController != nil && self.reactViewController.navigationController.interactivePopGestureRecognizer != nil) {
         [self.scrollView.panGestureRecognizer requireGestureRecognizerToFail:self.reactViewController.navigationController.interactivePopGestureRecognizer];
     }
@@ -494,4 +499,23 @@ - (NSString *)determineScrollDirection:(UIScrollView *)scrollView {
 - (BOOL)isLtrLayout {
     return [_layoutDirection isEqualToString:@"ltr"];
 }
+
+- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
+    if (otherGestureRecognizer == self.scrollView.panGestureRecognizer) {
+        UIPanGestureRecognizer* p = (UIPanGestureRecognizer*) gestureRecognizer;
+        CGPoint velocity = [p velocityInView:self];
+        if (self.currentIndex == 0 && velocity.x > 0) {
+            self.scrollView.panGestureRecognizer.enabled = false;
+            return NO;
+        } else {
+            self.scrollView.panGestureRecognizer.enabled = self.scrollEnabled;
+        }
+    } else {
+        self.scrollView.panGestureRecognizer.enabled = self.scrollEnabled;
+    }
+    
+    return YES;
+}
+
 @end
+

hirbod avatar Nov 30 '22 20:11 hirbod

Thank you!! This is so super helpful and works beautifully :)

sahil-ahuja-1 avatar Dec 01 '22 23:12 sahil-ahuja-1

@hirbod I'm seeing a very sporadic crash (maybe 1/30 times) I swipe back from a screen with this patch :( any ideas here?

Screen Shot 2022-12-08 at 10 16 03 PM

sahil-ahuja-1 avatar Dec 09 '22 03:12 sahil-ahuja-1

@sahil-ahuja-1 weird, hard to guess from the stack trace. Will try to repro. Can you upgrade the pager-view to 6.1.2 and try the below patch and check if that fixes the issue?

diff --git a/node_modules/react-native-pager-view/ios/ReactNativePageView.m b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
index cf2bd57..350d9b8 100644
--- a/node_modules/react-native-pager-view/ios/ReactNativePageView.m
+++ b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
@@ -9,7 +9,7 @@
 #import "RCTOnPageSelected.h"
 #import <math.h>
 
-@interface ReactNativePageView () <UIPageViewControllerDataSource, UIPageViewControllerDelegate, UIScrollViewDelegate>
+@interface ReactNativePageView () <UIPageViewControllerDataSource, UIPageViewControllerDelegate, UIScrollViewDelegate, UIGestureRecognizerDelegate>
 
 @property(nonatomic, strong) UIPageViewController *reactPageViewController;
 @property(nonatomic, strong) RCTEventDispatcher *eventDispatcher;
@@ -80,6 +80,11 @@
         [self setupInitialController];
     }
     
+    UIPanGestureRecognizer* panGestureRecognizer = [UIPanGestureRecognizer new];
+    self.panGestureRecognizer = panGestureRecognizer;
+    panGestureRecognizer.delegate = self;
+    [self addGestureRecognizer: panGestureRecognizer];
+
     if (self.reactViewController.navigationController != nil && self.reactViewController.navigationController.interactivePopGestureRecognizer != nil) {
         [self.scrollView.panGestureRecognizer requireGestureRecognizerToFail:self.reactViewController.navigationController.interactivePopGestureRecognizer];
     }
@@ -461,4 +466,29 @@
 - (BOOL)isLtrLayout {
     return [_layoutDirection isEqualToString:@"ltr"];
 }
+
+
+// The below snippet disables the pager view's scrollview's scroll when current index is 0 and user is swiping back. Useful for fullScreenGestureEnabled in RN Screens
+- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
+
+    // Recognize simultaneously only if the other gesture is RN Screen's pan gesture (one that is used to perform fullScreenGestureEnabled)
+    if (gestureRecognizer == self.panGestureRecognizer && [NSStringFromClass([otherGestureRecognizer class]) isEqual: @"RNSPanGestureRecognizer"]) {
+        UIPanGestureRecognizer* panGestureRecognizer = (UIPanGestureRecognizer*) gestureRecognizer;
+        CGPoint velocity = [panGestureRecognizer velocityInView:self];
+        BOOL isLTR = [self isLtrLayout];
+        BOOL isBackGesture = (isLTR && velocity.x > 0) || (!isLTR && velocity.x < 0);
+        
+        if (self.currentIndex == 0 && isBackGesture) {
+            self.scrollView.panGestureRecognizer.enabled = false;
+        } else {
+            self.scrollView.panGestureRecognizer.enabled = self.scrollEnabled;
+        }
+        
+        return YES;
+    }
+    
+    self.scrollView.panGestureRecognizer.enabled = self.scrollEnabled;
+    return NO;
+}
+
 @end

After hours of attempting to use the patch solutions found in this thread and none of them working (I am using a stack navigator), I am pleased to announce I have found the best solution to this issue:

 <View
      hitSlop={{ left: -15 }}
      style={{ flex: 1, width: '99.8%', alignSelf: 'center' }}
    >
      <TabView
        initialLayout={{ width: layout.width }}
        navigationState={{ index, routes }}
        onIndexChange={setIndex}
        renderScene={renderScene}
        renderTabBar={StyledTabBar}
      />
    </View>
  • wrap your TabView in a parent View
  • give it almost full width
  • add a hitslop

this works surprisingly really well. my back gesture is recognized by the most left tab, and doesn't interrupt any other gestures while swiping to other tabs. It's a bit hacky but literally the only thing I found to fix this. Hope this saves someone some time

branaust avatar Mar 09 '23 19:03 branaust