Search code examples
react-nativereact-navigationreact-native-gesture-handler

React Native - hide components on swipe


I have a layout where you can swipe horizontally using React Navigation 5's createMaterialTopTabNavigator or use a button on the bottom to transition between screens. The button appears on every screen and I would like to hide it during the swiping motion, and have it reappear when swiping stops.

Does anyone know if this is possible?

const Introduction1 = () => {
    return (
        <View style={styles.container}>
            <Text style={{ ...globalStyles.hTitle, ...globalStyles.h1 }}>Welcome</Text>
            <Text style={globalStyles.text}>
                Before you start there is a short introduction available.
            </Text>
            <Text style={{ ...globalStyles.text, ...globalStyles.bold }}>Do you want to see it?</Text>

            <BottomButtons name="Continue" destination="Introduction2" />
        </View>
    );
function SwiperNav() {
    return (
        <Swipe.Navigator
            tabBarOptions={{
                style: {
                    display: 'none'
                }
            }}>
            <Swipe.Screen name="Introduction1" component={Introduction1} />
            <Swipe.Screen name="Introduction2" component={Introduction2} />
            <Swipe.Screen name="Introduction3" component={Introduction3} />
            <Swipe.Screen name="Introduction4" component={Introduction4} />
            <Swipe.Screen name="Introduction5" component={Introduction5} />
        </Swipe.Navigator>
    );
}

enter image description here enter image description here


Solution

  • Original answer

    I recommend a different approach. Hiding elements on swipe is difficult (in my own experience) and is not recommended according to the react navigation documentation:

    Some tab navigators such as bottom tab navigator also have a tabBarVisible option which can be used to hide the tab bar based on instructions in the Screen options resolution guide. However, we don't recommend using it since showing/hiding the tab bar mid-navigation can affect the animation of the stack navigator causing glitchy behaviour.

    My approach is to define your continue button outside of your navigator.

    First create a RootNavigation file at your directory root that looks like this.

    import * as React from 'react';
    
    export const navigationRef = React.createRef();
    
    export function getCurrentRouteName() {
      return navigationRef.current?.getCurrentRoute().name;
    }
    
    export function navigate(name, params) {
      navigationRef.current?.navigate(name, params);
    }
    

    The idea of this RootNavigation file is to get access to the navigation prop and get the current route name from inside our button. This is otherwise not possible if the button is placed outside of a navigator.

    Pass the navigationRef to your NavigationContainer.

    import * as RootNavigation from './RootNavigation';
    // ...
    const App = () => {
      return (
        <NavigationContainer ref={RootNavigation.navigationRef}>
          <SwiperNav />
          <BottomButtons name="Continue" />
        </NavigationContainer>
      );
    }
    

    The following function is used to determine the next route the continue button should go to based on the current route name. If you want you can clean up the code by using constants or enums for the route names.

    const getSwipeNavigatorNextDestination = currentRouteName => {
      switch (currentRouteName) {
        case 'Introduction1':
          return 'Introduction2';
          break;
        case 'Introduction2':
          return 'Introduction3';
          break;
        // etc...
      }
    };
    

    I don't know what your BottomButtons component looks like exactly, but the important thing is that you define a function that handles onPress where you retrieve the next destination and navigate to that destination using RootNavigation.

    const BottomButtons = ({name}) => {
      const handleBtnNavigate = () => {
        const nextDestination = getSwipeNavigatorNextDestination(
          RootNavigation.getCurrentRouteName(),
        );
        RootNavigation.navigate(nextDestination)
      };
      return <Button title={name} onPress={() => handleBtnNavigate()} />;
    };
    

    If you want to read more about the RootNavigation idea you can refer to the documentation: https://reactnavigation.org/docs/navigating-without-navigation-prop/.

    If there is anything unclear you can comment and I can clarify.

    Extension to the original answer

    You should use only one NavigationContainer in your app. If you want to hide the continue button after navigating to a screen that is not part of the introductory screens, I recommend to change the following:

    const App = () => {
      const [isShowingIntroduction, setIsShowingIntroduction] = useState(true);
    
      return (
        <NavigationContainer ref={RootNavigation.navigationRef}>
          <SwiperNav />
          {isShowingIntroduction && (
            <BottomButtons
              name="Continue"
              setIsShowingIntroduction={setIsShowingIntroduction}
            />
          )}
        </NavigationContainer>
      );
    };
    

    The continue button is shown conditionally.

    const BottomButtons = ({name, setIsShowingIntroduction}) => {
      const handleBtnNavigate = () => {
        const nextDestination = getSwipeNavigatorNextDestination(
          RootNavigation.getCurrentRouteName(),
        );
        if (nextDestination == 'ScreenNotPartOfIntroduction') {
          setIsShowingIntroduction(false);
        }
        RootNavigation.navigate(nextDestination);
      };
      return <Button title={name} onPress={() => handleBtnNavigate()} />;
    };
    

    What I'm doing here is creating a state variable and accompanying setter in the app component (The parent component to the continue button). This allows us to pass the setter function to the continue button. Then inside the button component we can create a check: If nextDestination is a screen name not part of the set of introduction screen names, you call setIsShowingIntroduction(false). The app component updates and hides the button after which it navigates to nextDestination.