react native touchable highlight and touchable native feedback for making my menu items - react-native

I am trying to implement a basic drawer, but I am confused by the documentation.
There is a TouchableHighlight, and there is a TouchableNativeFeedback.
TouchableNativeFeedback is Android only. So, how can I best handle my MenuItem component so it takes care of both Android and IOS?
Here's what I've got so far, and I'm not sure how to handle the different platform specific touchable stuff here:
import React, { Component, PropTypes } from 'react';
import { TouchableHighlight, TouchableNativeFeedback, Platform, View, Text } from 'react-native';
export class MenuItem extends Component {
handleAndroid() {
}
handleIOS() {
}
renderMenuItem() {
return (
<View style={styles.container}>
<Text style={styles.text}>
{this.props.text}
</Text>
</View>
);
}
render() {
return (
);
}
}
On Android, do I have to use TouchableNativeFeedback only?

Like you said, TouchableNativeFeedback is Android only therefore it won't work for iOS. If you want a solution that works for both, use TouchableHighlight and style it accordingly. For example, its underlayColor prop allows you to set "the color of the underlay that will show through when the touch is active."
EDIT:
If you want to use TouchableNativeFeedback for Android and TouchableHighlight for iOS, you should do something like the following:
import { Platform } from 'react-native'
...
render() {
if (Platform.OS === 'android') {
return (
<TouchableNativeFeedback>
...
</TouchableNativeFeedback>
)
} else {
return (
<TouchableHighlight>
...
</TouchableHighlight>
)
}
}
EDIT 2:
Alternatively, you can create a custom component for each platform and use file extensions. For example:
MyButton.ios.js
MyButton.android.js
Put these two in the same folder and then use MyButton as a regular component:
import MyButton from '../path/to/component/MyButton'
...
render() {
<MyButton/>
}
This is quite neat when you want to use this component in multiple places because you don't fill your code with if-else blocks.
Here you can read more about Platform Specific Code.

You can solve this in a more elegant manner like this:
render() {
let TouchablePlatformSpecific = Platform.OS === 'ios' ?
TouchableOpacity :
TouchableNativeFeedback;
let touchableStyle = Platform.OS === 'ios' ?
styles.iosTouchable :
styles.androidTouchable
return(
<TouchablePlatformSpecific style={touchableStyle}>
(...)
</TouchablePlatformSpecific>
);
}
I've been using this successfully, and I fail to see a reason it would not work, it looks good to me.

A better implementation would be to have dumb component that produces generic Touchable that works depending upon the platform and handle onPress events.
Touchable Component:
import { Platform } from "react-native"
import { TouchableNativeFeedback, TouchableOpacity } from "react-native"
import React from "react"
export const Touchable = (props: any) => {
return Platform.OS === 'android'
? <TouchableNativeFeedback onPress={props.onPress}>{props.children}</TouchableNativeFeedback>
: <TouchableOpacity onPress={props.onPress}>{props.children}</TouchableOpacity>
}
Usage:
import { Touchable } from '../path/to/touchable.component';
...
render() {
<Touchable onPress={() => this.handleClick()}>
.....
</Touchable>
}
Thanks to #Giorgos' implementation which formed the basis for this answer.

Or you could create a custom component like this:
import { TouchableNativeFeedback, TouchableOpacity } from 'react-native'
...
const Touchable = (props) => {
return Platform.OS === 'android'
? <TouchableNativeFeedback>{props.children}</TouchableNativeFeedback>
: <TouchableOpacity>{props.children}</TouchableOpacity>
}
And then use it like this:
<Touchable>
<Card>
<Text>Click me!</Text>
</Card>
</Touchable>

I also have nearly the same requirements and I ended up using this library react-native-haptic-feedback.
Keep in mind that haptic feedback is available only on some latest android devices and in iOS above iPhone 6s. For devices without haptic motor, there will be vibration. Here is a sample code snippet:
import ReactNativeHapticFeedback from "react-native-haptic-feedback";
const options = {
enableVibrateFallback: true,
ignoreAndroidSystemSettings: false
};
ReactNativeHapticFeedback.trigger("impactMedium", options);
In your case, it will work directly with the button's onPress method such as:
<TouchableOpacity onPress={()=>{ReactNativeHapticFeedback.trigger("impactMedium", options);}} />
Note: I found that the most versatile type which is supported by maximum devices is impactMedium.

Based on #Nithish JV and Giorgos Karyofyllis:
import React from "react"
import { Platform } from "react-native"
import { TouchableNativeFeedback, TouchableOpacity } from "react-native"
export const TouchableFeedback = (props: any) => {
const { children, ...rest } = props;
return Platform.OS === 'android'
? <TouchableNativeFeedback {...rest}>{children}</TouchableNativeFeedback>
: <TouchableOpacity {...rest}>{children}</TouchableOpacity>
}

The best elegant way to do it
import { TouchableNativeFeedback, TouchableOpacity } from 'react-native'
in render:
TouchableButton = Platform.OS === 'android' ? TouchableNativeFeedback : TouchableOpacity
return (
<TouchableButton>{'bla bla'}</TouchableButton>
)

Since the operating system doesn't change magically during runtime, I always use this little snippet:
import { Component } from "react"
import {
Platform,
TouchableNativeFeedback,
TouchableOpacity,
} from "react-native"
// The operating system doesn't magically change,
// so we can call the function directly to get the correct component
export default ((): typeof Component => {
if (Platform.OS === "android") {
return TouchableNativeFeedback
}
return TouchableOpacity // Choose whatever touchable you like
})()
It's basically an IIFE that selects the correct touchable for the operating system.

Related

Workaround expo react native web splashscreen

Is there any existing workaround to show a splashscreen on web? It is not yet supported, and I'd like to avoid seeing a white screen while loading the website.
Ref.: https://docs.expo.io/versions/v41.0.0/sdk/splash-screen/
Known issue on github: https://github.com/expo/expo/issues/10839
I tested (and use) it with SDK 47 and adapted the example on https://docs.expo.dev/versions/latest/sdk/splash-screen/#usage like this (I simplified some components and functions here for better readability, so this example never "run" like this in reality):
import React, { useEffect, useState } from 'react';
import { Text, View, Platform } from 'react-native';
import Entypo from '#expo/vector-icons/Entypo';
import * as SplashScreen from 'expo-splash-screen';
import * as Font from 'expo-font';
import { runAllTheInitStuff } from './init';
import SomeProvider from './SomeProvider';
import AnotherProvider from './AnotherProvider';
import WebSplashScreen from './WebSplashScreen';
// Keep the splash screen visible while we fetch resources
SplashScreen.preventAutoHideAsync();
export default function App() {
const [appIsReady, setAppIsReady] = useState(false);
useEffect(() => {
async function prepare() {
await runAllTheInitStuff();
// Tell the application to render
setAppIsReady(true);
// hide splash screen
await SplashScreen.hideAsync();
}
prepare();
}, []);
// check if app is ready
if(!appIsReady) {
// check if we are in web
if(Platform.OS === 'web') {
return <WebSplashScreen />;
} else {
return null;
}
}
return (
<SomeProvider>
<AnotherProvider>
<View
style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>SplashScreen Demo! 👋</Text>
<Entypo name="rocket" size={30} />
</View>
</AnotherProvider>
</SomeProvider>
);
}
I do not use a <View> component as first entry point, but a lot of provider, so it would be quite challenging to use onLayout prop in my case. That's the reason why hiding the splash screen is done directly in the useEffect hook...
The WebSplashScreen component can be anything (i.e. the splash screen used in mobile app as image or what ever), I use a simple activity indicator from a material ui library...

NativeBase: Button does not work, but ReactNative's Button does

Experiencing a strange issue in my React Native project for Android.
Using React-Navigation, I have a component with a button inside. This button should navigate to a new screen.
Thing is, the built-in button of React Native works like a charm, while the button of Native Base does not. I am completely confused, even more because I use this Native Base Button in another location, too. And there it works fine.
What is going on here?
Here, you see the application works with the built-in React Native button:
On the opposite, using the button of Native Base, it not only does not work, even styles are not applied.
Here is the code with the React Native button:
import React from "react";
import { Button, View, Text, StyleSheet } from "react-native";
import { withNavigation } from "react-navigation";
type Props = { navigation: any };
const ButtonTestScreen: React.FC<Props> = ({ navigation }) => {
return (
<View>
<Button
title="Hi i am a button"
onPress={() => navigation.navigate("Details")}
></Button>
</View>
);
};
export default withNavigation(ButtonTestScreen);
And the code with Native Base button:
import React from "react";
import { Button, View, Text, StyleSheet } from "react-native";
import { withNavigation } from "react-navigation";
import ButtonNavigate from "../../components/atoms/ButtonNavigate/ButtonNavigate";
type Props = { navigation: any };
const ButtonTestScreen: React.FC<Props> = ({ navigation }) => {
return (
<View>
<ButtonNavigate
title="Hi i am a button"
navigateTo="Details"
></ButtonNavigate>
</View>
);
};
const styles = StyleSheet.create({
button_style: {
backgroundColor: "red"
},
text_style: {
color: "#000",
fontSize: 30
}
});
export default withNavigation(ButtonTestScreen);
And the respective ButtonNavigate component itself:
import React from "react";
import { StyleSheet } from "react-native";
import { withNavigation } from "react-navigation";
import { Button, Text } from "native-base";
type Props = {
title: string,
navigateTo: string,
navigation: any
};
const ButtonNavigate: React.FC<Props> = ({ title, navigateTo, navigation }) => {
return (
<Button
rounded
transparent
style={styles.button_style}
onPress={() => navigation.navigate(navigateTo)}
>
<Text style={styles.text_style}>{title}</Text>
</Button>
);
};
const styles = StyleSheet.create({
button_style: {
backgroundColor: "red"
},
text_style: {
color: "#151414"
}
});
export default withNavigation(ButtonNavigate);
I have just tested you code in expo.snack but without navigation and its ok,
see it here
You can test in your app to remove navigation and go step by step until you find the bug.
Folks, reason for this strange behavior is the "rounded" property of Native Base's button. In my application, somehow it causes the button to become non-clickable.
Maybe contributors of Native Base know what to do with this problem, so if you read this, maybe you have an idea.
Solution for my now was simply removing "rounded".
Native Base: 2.13.8
React-Navigation: 4.0.10
In my case it was the "top" in the container property of the button causing this issue. Removed it and adding "marginBottom" to the container above it solved the issue

React-Native, ActionSheetIOS display instead of Picker

I would like to use the ActionSheetIOS on iOS, instead of the native picker wheel.
My app crashes out, how can I display my component?
Here is my Picker component:
// Picker.ios.js
import React, { Component } from "react";
import { StyleSheet, ActionSheetIOS, View } from "react-native";
const PickerList = props => {
const { label, options, selectedValue, name, onChange, identifier } = props;
return ActionSheetIOS.showActionSheetWithOptions(
{
options: options,
cancelButtonIndex: 1,
destructiveButtonIndex: 2
},
optionIndex => {
console.log('clicked')
}
);
};
export default PickerList;
I'm using conditional rendering to display my pickers, and a platform specific import:
import Picker from "./common/Picker";
{setYear
? <Picker
selectedValue={setGroup}
label="Year group"
onChange={this.onGroupChange}
options={
categories.find(category => {
return category.id == setYear;
}).options
}
/>
: null}
ActionSheetIOS only shows the options, you have to have some view component to replace the picker in the view. I used TouchableOpacity ja ActionSheetIOS to replace Picker on IOS like this:
<TouchableOpacity onPress={this.onSelectCategory.bind(this)}>
<Text style={styles.textInputStyle}>
{this.state.category}
</Text>
</TouchableOpacity>
onSelectCategory() {
ActionSheetIOS.showActionSheetWithOptions(
{ options: FEEDBACK_CATEGORIES},
(buttonIndex) => this.setState({ category: FEEDBACK_CATEGORIES[buttonIndex] }));
}
I use this.state.category to show my selection in TouchableOpacity
When user press the TouchableOpacity the ActionSheetIOS is shown
When user select option the callback function is called and I update the
selected index to the this.state.category

Keyboard listener is running more then once

Hey I'm trying to create an event that will fire when the keyboard shows up but the function is firing more then once, I don't know why ..
import React, { Component } from 'react';
import { Keyboard, Alert, View, TextInput } from 'react-native';
export default class App extends Component {
constructor(props: any) {
super(props);
this.kbDidShowListener = Keyboard.addListener('keyboardDidShow', () => Alert.alert('keyboard is up'));
}
componentWillUnmount() {
this.kbDidShowListener.remove();
}
render() {
return (
<View style={{ marginTop: 30 }}>
<TextInput />
</View>
);
}
}
here is an expo for the example (you will see the alert more then once)
https://snack.expo.io/H1DHaIdgM
p.s I'm working on Android.
thanks!
The render function does not run only once. Usually refreshes multiple times too, while calculating the state and props. That could explain the issue.
If you want to be sure, try adding a console too inside the render method, to see if the numbers match.
Actually, another thing I am thinking. Try moving the code to the componentWillMount or componentDidMount
componentDidMount(){
this.kbDidShowListener = Keyboard.addListener('keyboardDidShow', () => Alert.alert('keyboard is up'));
}

how could I swipe the page from left to right in order to go back in a webview?

In a web view,I want to swipe the page from left to right in order to go back,just like what safari did.
what should I do?
TL/DR: Currently, you'll need a third party package for that. Use the react-native-wkwebview-reborn package and set the allowsBackForwardNavigationGestures prop to true. Example:
WebView.ios.js:
import React from 'react';
import WKWebView from 'react-native-wkwebview-reborn';
export default (props) => <WKWebView allowsBackForwardNavigationGestures {...props} />
WebView.js
import { WebView } from 'react-native';
export default WebView;
It's a drop-in replacement, so you won't need to change much code.
Why:
The WebView component from React Native uses UIWebView under the hood, which is not recommended by Apple anymore:
It has worse performance and does not support a lot of features, like 3D Touch and swipe back gesture.
Join this discussion so react native updates their core component.
You can use react-native-webview package. And just pass allowsBackForwardNavigationGestures prop to WebView:
import WebView from 'react-native-webview';
<WebView
allowsBackForwardNavigationGestures
...
/>
I know it's been a long time but I came across with the same problem and couldn't find any working solutions for android. So I came up with mine. Also, this post is first to show up when you google 'react native Webview swipe back', so I thought it would help more people posting my solution here.
import * as React from "react";
import { WebView } from "react-native-webview";
import { StyleSheet, Platform, BackHandler } from "react-native";
export default function App() {
const webViewRef = React.useRef();
const onAndroidBackpress = () => {
if (webViewRef.current) {
webViewRef.current.goBack();
return true;
}
return false;
};
return (
<WebView
onTouchStart={e => {
if(Platform.OS === 'android'){
this.touchX = e.nativeEvent.pageX;
this.touchY = e.nativeEvent.pageY
} else {
return null
}}
}
onTouchEnd={e => {
if(Platform.OS === 'android' && this.touchX - e.nativeEvent.pageX < -20){
if(this.touchY - e.nativeEvent.pageY > -20 && this.touchY - e.nativeEvent.pageY < 20){
onAndroidBackpress()
}
} else {
return null
}}
}
allowsBackForwardNavigationGestures // only works with iOS
allowsInlineMediaPlayback
ref={webViewRef}
style={styles.container}
source={{ uri: "https://<yourwebsite>.com" }}
/>
);
}
const styles = StyleSheet.create({
container: {
flex: 1
}
});