I am building an app that has three tab navigation.
Each tab wraps a WebView and displays the uri webview is directing.
So, I have three WebViews running in an app.
/* each screen wraps a webview */
/screens
-HomeScreen
-ChatScreen
-ProfileScreen
In each screen, I have back button mapped to call webviewRef.current.goBack() just like below.
useEffect(() => {
const handleBackButtonPress = () => {
if (webviewRef.current) {
webviewRef.current.goBack();
return true;
}
return false;
};
BackHandler.addEventListener("hardwareBackPress", handleBackButtonPress);
return () =>
BackHandler.removeEventListener(
"hardwareBackPress",
handleBackButtonPress
);
}, []);
return (
<Screen>
<WebView
source={{
uri: "http://0.0.0.0:3000/",
}}
ref={webviewRef}
onMessage={handleMessage}
injectedJavaScript={script}
/>
</Screen>
);
It works seamlessly in each webviews, but the problem happens when I move on to different screen(or webview). To help you grasp, I will list the steps to generate the problem I faced.
I open the app and view HomeScreen webview.
I look around in HomeScreen webview (I can use back button)
I decide to move to ProfileScreen and touch profile icon in the tab navigation
I am on ProfileScreen webview.
I look around in ProfileScreen webview (I visit links)
I touch hardware back button and it does not work. (to elaborate, it does nothing.)
My guess is:
the cleanup function in useEffect is somehow removing 'hardwareBackPress' event..
advice appreciated.
Related
I am trying to hide app content when the user tries to navigate away from the app, like how banking apps or outlook works. I am using appstate inactive for ios, and appstate blur+focus for android. This works on Android when the user pulls down the notification panel or swipes up to multitasking tray or home through a gesture.
However, if the android phone has soft buttons for navigation instead of gestures, the app doesnt show the security screen on blur. I suppose react native loses control of the app too soon for that to happen. The blur event is still triggered, but the UI remains unchanged.
I have a useEffect in a custom hook that changes the state on blur, which triggers the UI update;
const [appStateVisible, setAppStateVisible] = useState(true);
const appState = useRef(AppState.currentState);
useEffect(() => {
const androidFocusSubscription =
Platform.OS === 'android' &&
AppState.addEventListener('focus', () => {
setAppStateVisible(true);
});
const androidBlurSubscription =
Platform.OS === 'android' &&
AppState.addEventListener('blur', () => {
setAppStateVisible(false);
});
return () => {
if (androidFocusSubscription && androidBlurSubscription) {
androidFocusSubscription.remove();
androidBlurSubscription.remove();
}
};
}, []);
return appStateVisible;
And then on App.tsx, I am calling this hook to decide whether to show the security image instead of the actual app content.
The alternative would be to use FLAG_SECURE in the java code to show a white screen when the app isnt active, but I want to show a custom screen with the app logo instead of a white screen.
Any help would be greatly appreciated. Thank you.
I currently have a react native app with one main screen that navigates to a second screen that has 3 text inputs. The problem I am having only occurs on Android. On the second screen, whenever a text input is in focus and I try to navigate back via react navigations’ back button, the keyboard hides, then reappears, then hides, and then transitions back to the first screen. It seems when I initially press the back button the inputs focus is blurred, then when I let go it refocuses on the input and then blurs it again and then navigates back. Not sure why this is happening on Android. It works as expected on iOS. Any solutions?
Thanks!
Did you have been manipulating keyboardHandlingEnabled prop? It can be related to this.
Or you can just hook back press event using This Guide and blur your text input first like following.
function ScreenWithCustomBackBehavior() {
// ...
useFocusEffect(
React.useCallback(() => {
const onBackPress = () => {
if (isSelectionModeEnabled()) {
disableSelectionMode();
return true;
} else {
return false;
}
};
BackHandler.addEventListener('hardwareBackPress', onBackPress);
return () =>
BackHandler.removeEventListener('hardwareBackPress', onBackPress);
}, [isSelectionModeEnabled, disableSelectionMode])
);
// ...
}
I am experiencing a very weird behaviour in my android emulator when using react-navigation v5 in my react native application, which makes me think is a bug. I found out that there is an open issue on the official react-native-navigation page. See here.
In every secondary screen (see UserProfileScreen below), in many react native-elements like TouchableOpacity Button or TextInput the onPress event doesn't work. It only works in the main screen of the navigator. (See HomeScreen below)
Here is an example of how I create my stack navigator:
import {createStackNavigator} from '#react-navigation/stack';
// some imports here
const HomeStackNavigator = createStackNavigator();
export const HomeNavigator = () => {
return (
<HomeStackNavigator.Navigator>
<HomeStackNavigator.Screen name="HomeScreen" component={HomeScreen}/>
<HomeStackNavigator.Screen name="UserProfileScreen" component={UserProfileScreen}/>
<HomeStackNavigator.Screen name="UserSettingsScreen" component={UserSettingsScreen}/>
</HomeStackNavigator.Navigator>
)
};
As a PoC I have the same code in the main and secondary screen:
<TouchableOpacity
onPress={() => Alert.alert("You pressed me!")} >
<Text> touch me</Text>
</TouchableOpacity>
and I see the alert only in the main screen (HomeScreen).
If I make UserProfileScreen as the fist screen in my stack navigator above then it works (the onPress event) fine on this screen but not in the HomeScreen. So it seems that the onPress event is triggered only in the main screen!. On IOS it works fine in all screens. Any idea ? Let me know if you need some more code snippets.
I'm hoping this is a known problem as I can't provide much to go on to get help solving it.
I'm using react-navigation for my app with the following setup:
AppContainer
Splash Screen
Setup Screen (A stack navigator)
Main screen (A stack navigator)
When my app starts it goes to the splash screen which decides if this is the first time running or not and depending on this decision calls this.props.navigation.navigate() with either main screen or setup screen.
So far this all works and is fairly standard.
When my app is launched for the first time the user is sent to the setup screen where they navigate through a series of screens entering data and selecting a next button to proceed to the next screen. This is where the problem occur. My first screen simply has some text and a next button (which is a regular button component) which when clicked calls this.props.navigation.push('nextviewname', {data: data}) to move to the next view.
The next view contains a textinput as well as back and next buttons which is where I'm having problems. When I reach this screen after freshly installing a release version of my app onto my Android X phone none of the inputs work. I can't:
Click next of back
Click the back arrow in the top left that is part of the header
Click the text input (the cursor does briefly show up in the text input but the keyboard never appears)
Click the hardware back key
On very rare occasions some of the above does work (e.g. the text input will sometimes work) and sometimes I'll even make it to the next step of my setup but it's rare that I make it all the way through
Weirdly this all works when I'm debugging my app on my phone.
Update: I've just tested on an Android 9 emulator and I'm getting the same issue.
Update 2: This is getting weird, when the app is in a broken state I can still bring up the react native debug menu however when I click Toggle Inspector nothing happens (i.e. I don't get the inspector UI). It's looking like this is somehow breaking everything.
Has anyone seen/solved this issue before? At the moment it's effectively made my app useless.
Update 3: Some code to hopefully make things clearer:
const SetupUser = createStackNavigator(
{
SetupUser: WelcomeScreen,
SetupName: UserName,
SetupCurrentWeight: CurrentWeight,
SetupGoalWeight: GoalWeight,
SetupGoalDate: GoalDate,
Summary: Summary,
LogWeight: LogWeight,
},
{
defaultNavigationOptions: {
headerStyle: {
backgroundColor: '#001830',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
},
},
);
const MainApp = createStackNavigator(
{
LogWeight: LogWeight,
LogWeightSummary: LogWeightSummary,
},
{
defaultNavigationOptions: {
headerStyle: {
backgroundColor: '#001830',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
},
},
);
export default createAppContainer(
createSwitchNavigator(
{
MainApp: MainApp,
SplashScreen: SplashScreen,
SetupUser: SetupUser,
},
{
initialRouteName: 'SplashScreen',
},
),
);
In getting this code snippit together (I've removed the tab navigator as the error is still there even without it) I think I've managed to track down the source of the issue however I'm still not sure how to fix it. The first view loaded is the splash screen which looks like this:
export default class SplashScreen extends React.Component {
constructor(props) {
super(props);
GoBinder.verifyDatabase()
.then(GoBinder.getLastUser())
.then(user => {
this.props.navigation.navigate(
user.user == null ? 'SetupUser' : 'MainApp',
);
})
.catch(error => {
GoBinder.toast('Error while checking initial user state: ' + error);
});
}
render() {
return (
<View style={styles.container}>
<ActivityIndicator />
<StatusBar barStyle="default" />
</View>
);
}
}
In the above code, GoBinder.verifyDatabase() and GoBinder.getLastUser() are calls to native code which perform some database operation to get things setup and check to see if there are any existing users. I believe the issue is this.props.navigation.navigate is firing too quickly which is causing react-navigation to load the next screen but get messed up in the process.
I've tried following the suggestions in this post https://www.novemberfive.co/blog/react-performance-navigation-animations about moving blocking code into InteractionManager.runAfterInteractions under componentDidMount however this made no difference. However, it I manually trigger the move to the new screen using a button everything works correctly so its really looking like the act of programatically changing screens is messing things up.
Update 4:
Looks like I jumped the gun a bit, its still freezing up a fair bit, moving my answer into an update:
So I'm not sure if this is the best solution but I have found a workaround. I am now calling my database code in InteractionManager.runArfetInteraction() as suggested in https://www.novemberfive.co/blog/react-performance-navigation-animations. I am then calling setState with the result of the DB functions to store the result. Finally, I am using a callback on setState to trigger the actual navigation. My complete working code is:
componentDidMount() {
InteractionManager.runAfterInteractions(() => {
GoBinder.verifyDatabase()
.then(
GoBinder.getLastUser().then(user => {
this.setState(
{
user: user.user,
},
() => {
this.props.navigation.navigate(
this.state.user == null ? 'SetupUser' : 'MainApp',
);
},
);
}),
)
.catch(error => {
GoBinder.toast('Error while checking initial user state: ' + error);
});
});
}
Thanks for your help
So i am building an app where there will be a page just for a gif animation. I want so that after that gif animation is done, it will automatically go to the next page. How do i do that?
Am i supposed to use a timer, so that the gif page will only appear for 3 seconds and then it goes to the next page? Or what is the best way to approach this?
how i manage routing:
render() {
return (
<Navigator
renderScene={this.renderScene.bind(this)}
navigator={this.props.navigator}
/>
);
}
renderScene(route, navigator) {
return (
...);
}
Well it's not clear from your question what navigation scheme you're using, but regardless, the process is trivially done if the length of the GIF is known. If it's unknown, I suspect you're in for a lot more work, but otherwise simply:
const GIF_LENGTH = 5 * 1000; // 5 second gif
class MyComponent extends React.Component {
componentDidMount() {
setTimeout(() => nav.push({ name: 'NextPage' }), GIF_LENGTH);
}
render() {
return < gif display here >;
}
}
Obviously replace nav.push(...) with the particulars of however you manage routing in your application.