PedroBern / react-native-collapsible-tab-view

A cross-platform Collapsible Tab View component for React Native
MIT License
829 stars 160 forks source link

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

Closed hirbod closed 2 weeks ago

hirbod commented 2 years ago

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 commented 2 years ago

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.

andreialecu commented 2 years ago

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.

hirbod commented 2 years ago

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 commented 2 years ago

@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.

vbylen commented 2 years ago

@hirbod try doing it like this:

<Tabs.Container pagerProps={{ hitSlop: { left: -50 }}} />
hirbod commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

This is broken with the current RC (version 5)

hirbod commented 2 years ago

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

mikefogg commented 1 year ago

@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!

hirbod commented 1 year ago

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
+
sahil-ahuja-1 commented 1 year ago

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

sahil-ahuja-1 commented 1 year ago

@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
intergalacticspacehighway commented 1 year ago

@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
branaust commented 1 year ago

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>

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