Implementing iOS picker in React Native — Part 1

Gogul Bharathi Subbaraj
6 min readDec 24, 2022

--

A tutorial on implementing an iOS style picker, purely in react native using FlatList.

Picker is a common component used in mobile UX development, but its iOS and Android variants are very different. This can be an issue if you want your design to be consistent across these platforms in a react native app.

In this article we will implement a picker similar to the iOS variant, entirely in react native, using the FlatList component.

Overview of approach

Many of us would have used the FlatList component in react native for rendering a dynamic list of items. By using the Animated version of the same and by adding a few props like snapToInterval, and doing a bit of math to get the selected item index, we would arrive at a fully functional picker, which might be simple, yet can do the trick

The design and colors used here might be simple, but they can be tweaked to suit your needs.

Listing Items

In this step, we will just render the list of items from which an item needs to be selected. No magic here, just pass items to FlatList & render them. We will create a new component called WheelPicker,

import React from 'react';
import {
FlatList,
ListRenderItemInfo,
StyleSheet,
Text,
View,
} from 'react-native';

interface Props {
items: string[];
onIndexChange: (index: number) => void;
itemHeight: number;
}

const WheelPicker: React.FC<Props> = props => {
const {items, onIndexChange, itemHeight} = props;

const renderItem = ({item}: ListRenderItemInfo<string>) => {
return (
<Text style={[styles.pickerItem, {height: itemHeight}]}>{item}</Text>
);
};

return (
<View style={{height: itemHeight * 3}}>
<FlatList
data={items}
renderItem={renderItem}
showsVerticalScrollIndicator={false}
/>
</View>
);
};

const styles = StyleSheet.create({
pickerItem: {
fontSize: 18,
fontWeight: '600',
textAlign: 'center',
color: '#000',
},
});
export default WheelPicker;

One prop to note here itemHeight, which is passed to the component specifying height of each item in the picker. We are also limiting height of the component to 3 times the itemHeight, so that only 3 items are visible.

Adding Indicators for the selected item

Let’s add indicators so that the user can know which item is selected. The design of indicators can be changed accordingly to design needs.

// remaining code
// ...

return (
<View style={{height: itemHeight * 3}}>
<FlatList
data={items}
renderItem={renderItem}
showsVerticalScrollIndicator={false}
/>
<View style={[styles.indicatorHolder, {top: itemHeight}]}>
<View style={[styles.indicator]} />
<View style={[styles.indicator, {marginTop: itemHeight}]} />
</View>
</View>
);

};

const styles = StyleSheet.create({
// other styles
// ...

indicatorHolder: {
position: 'absolute',
},

indicator: {
width: 120,
height: 1,
backgroundColor: '#ccc',
},
});

Two things need fixing now:

  • The first and last items are not selectable
  • Scroll position can go in between 2 items

Modifying list and Snapping items

Let’s first add empty strings to the first and last index, so that the items in the original list are selectable by scrolling


...
const modifiedItems = ['', ...items, ''];

return (
<View style={{height: itemHeight * 3}}>
<FlatList
data={modifiedItems}
...
/>
...
</View>
...
)

Add snapToInterval as item height, so that items are snapped properly.

<FlatList
data={modifiedItems}
snapToInterval={itemHeight}
...
/>

First and last items are selectable and also items are getting snapped properly now.

Get selected item

We will be using onMomentumScrollEnd prop of FlatList to get the scroll position and apply some math to get the selected item index.

...

const momentumScrollEnd = (
event: NativeSyntheticEvent<NativeScrollEvent>,
) => {
const y = event.nativeEvent.contentOffset.y;
const index = Math.round(y / itemHeight);
props.onIndexChange(index);
};

return (
<View style={{height: itemHeight * 3}}>
<FlatList
...other props
onMomentumScrollEnd={momentumScrollEnd}
/>
...
</View>
);

...

When the scroll ends, we get the y offset, and divide it by the item height to get the selected index. With this index, we are calling the props provided onIndexChange callback.

Adding animations

We will replace the normal FlatList with the Animated counterpart to use interpolation and animated scroll events to achieve an animation similar to the native iOS one.


...

const scrollY = useRef(new Animated.Value(0)).current;

const renderItem = ({item, index}: ListRenderItemInfo<string>) => {

const inputRange = [
(index - 2) * itemHeight,
(index - 1) * itemHeight,
index * itemHeight,
];

const scale = scrollY.interpolate({
inputRange,
outputRange: [0.8, 1, 0.8],
});

return (
<Animated.View
style={[
{height: itemHeight, transform: [{scale}]},
styles.animatedContainer,
]}>
<Text style={styles.pickerItem}>{item}</Text>
</Animated.View>
);
};

...

return (
<View style={{height: itemHeight * 3}}>
<Animated.FlatList
data={modifiedItems}
renderItem={renderItem}
showsVerticalScrollIndicator={false}
snapToInterval={itemHeight}
onMomentumScrollEnd={momentumScrollEnd}
scrollEventThrottle={16}
onScroll={Animated.event(
[{nativeEvent: {contentOffset: {y: scrollY}}}],
{useNativeDriver: true},
)}
/>
...
</View>
);

In the above code, we have done the following changes,

Some optimizations

Since each item has a fixed layout size, it is better to implement getItemLayout on the FlatList to have a performance improvement. Also, implement keyExtractor callback which is required for a FlatList.

...

<Animated.FlatList
data={modifiedItems}
... other props
getItemLayout={(_, index) => ({
length: itemHeight,
offset: itemHeight * index,
index,
})}
/>

...

And, voilà!

With the complete code below, you must be able to get a beautiful iOS-style picker, which is simple yet does the trick.

import React, {useRef} from 'react';
import {
Animated,
ListRenderItemInfo,
NativeScrollEvent,
NativeSyntheticEvent,
StyleSheet,
Text,
View,
} from 'react-native';

interface Props {
items: string[];
onIndexChange: (index: number) => void;
itemHeight: number;
}

const WheelPicker: React.FC<Props> = props => {
const {items, onIndexChange, itemHeight} = props;

const scrollY = useRef(new Animated.Value(0)).current;

const renderItem = ({item, index}: ListRenderItemInfo<string>) => {
const inputRange = [
(index - 2) * itemHeight,
(index - 1) * itemHeight,
index * itemHeight,
];
const scale = scrollY.interpolate({
inputRange,
outputRange: [0.8, 1, 0.8],
});

return (
<Animated.View
style={[
{height: itemHeight, transform: [{scale}]},
styles.animatedContainer,
]}>
<Text style={styles.pickerItem}>{item}</Text>
</Animated.View>
);
};

const modifiedItems = ['', ...items, ''];

const momentumScrollEnd = (
event: NativeSyntheticEvent<NativeScrollEvent>,
) => {
const y = event.nativeEvent.contentOffset.y;
const index = Math.round(y / itemHeight);
onIndexChange(index);
};

return (
<View style={{height: itemHeight * 3}}>
<Animated.FlatList
data={modifiedItems}
renderItem={renderItem}
showsVerticalScrollIndicator={false}
snapToInterval={itemHeight}
onMomentumScrollEnd={momentumScrollEnd}
scrollEventThrottle={16}
onScroll={Animated.event(
[{nativeEvent: {contentOffset: {y: scrollY}}}],
{useNativeDriver: true},
)}
getItemLayout={(_, index) => ({
length: itemHeight,
offset: itemHeight * index,
index,
})}
/>
<View style={[styles.indicatorHolder, {top: itemHeight}]}>
<View style={[styles.indicator]} />
<View style={[styles.indicator, {marginTop: itemHeight}]} />
</View>
</View>
);
};

const styles = StyleSheet.create({
pickerItem: {
fontSize: 18,
fontWeight: '600',
textAlign: 'center',
textAlignVertical: 'center',
color: '#000',
},
indicatorHolder: {
position: 'absolute',
},
indicator: {
width: 120,
height: 1,
backgroundColor: '#ccc',
},
animatedContainer: {
justifyContent: 'center',
alignItems: 'center',
},
});

export default WheelPicker;

Wrapping Up

Animated.FlatList is a heavy component and should be used with caution. Remember to memoize the component to prevent unwanted re-renders. There are some ready-made libraries available as well, but if the use case you are trying to solve is simple and want to try out some Animated side of react-native, I guess this tutorial would have been a good reference material.

--

--