Implement SwipeableListView sample in React Native

I’ve been developing music player application using SoundCloud APIs and this is a story while I had been working on a feature about editing playlist, especially removing tracks from the list.

First, here is the output of this story

Sample application

To implement a listview which can be swipeable, I googled two open-source components.

However, I had found an interesting article that RN already added the component that I need since v0.27.0. 👍

stackoverflow

It’s an experimental feature and there still doesn’t exist official document about how to use it so it’s not easy to find it for some people like me. 😞

https://github.com/facebook/react-native

Anyway, SwipeableListView is basically an extended component from ListView. Therefore, except swipe-related features, it is similar to ListView for usage.

Create data source

First, we need to create data source with row data but the way of creating data source it is a bit different between the two.

Below is the general way to create data source for ListView .

const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
this.state = {
dataSource: ds.cloneWithRows(tracks),
};

Below is how data source for SwipeableListView is created.

const ds = SwipeableListView.getNewDataSource();
this.state = {
dataSource:
ds.cloneWithRowsAndSections(...this.genDataSource(tracks)),
};

Here we find two differences: one is how to create DataSource object and the other is SwipeableListView doesn’t support cloneWithRows method. So you should manage dataBlob, sectionIdentities, rowIdentities and this is done bygenDataSource method.

genDataSource(rowData: Array<any>) {
const dataBlob = {};
const sectionIDs = ['Section 0'];
const rowIDs = [[]];

/**
* dataBlob example below:
{
'Section 0': {
'Row 0': {
id: '0',
text: 'row 0 text'
},
'Row 1': {
id: '1',
text: 'row 1 text'
}
}
}
*/
dataBlob['Section 0'] = {};
rowData.forEach((el, index) => {
const rowName = `${index}`;
dataBlob[sectionIDs[0]][rowName] = {
id: rowName,
...el,
};
rowIDs[0].push(rowName);
});
return [dataBlob, sectionIDs, rowIDs];
}

It’s important to recognize that when you use cloneWithRows you just need to pass row data(mostly array) as dataBlob but in this case row data needs to be tweaked for later use. Simple format is addressed in the code snippet above.

renderRow and renderQuickActions

renderRow is pretty much same as the one in ListView. But if you close the selected row when clicking open row, you should check whether there exists any row currently open.

onSelectRow(rowID) {
const openID = this.state.dataSource.getOpenRowID();
if (openID && (openID === rowID)) {
this.setState({
dataSource: this.state.dataSource.setOpenRowID(null),
});
}
// Add more codes
}
renderRow(rowData, sectionID, rowID) {
const { title, artist, image_url } = rowData;
return (
<TouchableHighlight onPress={() => this.onSelectRow(rowID)}>
... 생략 ...
</TouchableHighlight>
);
}

getOpenRowID is a method to return rowID of currently open row. If there is no open row it returns null. So to close open row you can compare the two values(selected row id and currenly open row id) and pass null to setOpenRowID in order to close the row.

The areas that are visible when user swipe out are controlled by renderQuickActions prop and there are two ways to render quick actions.

  1. Pre-defined standard component
  2. User-defined custom component

In the first case, you can combine SwipeableQuickActions and SwipeableQuickActionButton to render quick actions in an easy way.

renderStandardQuickActions(rowData: object, sectionID: string, rowID: string) {
return (
<SwipeableQuickActions>
<SwipeableQuickActionButton
imageSource={{ uri: 'https://unsplash.it/300/300?random' }}
imageStyle={styles.thumbnail}
onPress={() => { }}
text={'Action'}
textStyle={{ color: 'white' }}
/>
</SwipeableQuickActions>
);
}

However as you see above SwipeableQuickActionButton receive image and text props which means it contains Image and Text components internally. And in case of Image it’s required property. (I don’t know why it is required prop because I cannot find any useful use cases with them)

Therefore my render method returns custom action buttons.

onDeleteRow(index) {
tracks = [
...tracks.slice(0, index),
...tracks.slice(index + 1),
];
const ds = SwipeableListView.getNewDataSource();
this.setState({
dataSource: ds.cloneWithRowsAndSections(...this.genDataSource(tracks)),
});
}
renderCustomQuickActions(rowData: object, sectionID: string, rowID: string) {
return (
<View style={styles.actionsContainer}>
<TouchableHighlight
style={[styles.actionButton, { backgroundColor: 'gold' }]}
underlayColor="transparent"
onPress={() => console.log(rowID)}>
<Icon name="pin" size={20} />
</TouchableHighlight>
<TouchableHighlight
style={[styles.actionButton, { backgroundColor: 'goldenrod' }]}
underlayColor="transparent"
onPress={() => this.onDeleteRow(parseInt(rowID))}>
<Icon name="trash" size={20} />
</TouchableHighlight>
</View>
);
}

Wrap up

Done! isn’t it easy?SwipeableListView contains very basic features for swipeout in a listview. If you need more complex animation or more customization you should use third-party component but if not, SwipeableListView can be a good candidate, I guess!

You can see full source code and test sample application over snack. Also you can also Korean version of this story.