Announcing React Native Copilot: A React Native Package to Create In-App Walk-throughs


Introduction

A common requirement for many apps today is a UI walk-through. There are a number of packages that exist for the web such as Intro.js – a VanillaJS package, and Joyride – a React library, but none that work specifically for React Native apps. This is why we created React Native Copilot—a package that lets you easily create step-by-step walk-throughs for React Native apps.

A demo of React Native Copilot

Getting started

Let’s start with implementing a simple screen.

import React from 'react';
import { StyleSheet, Text, Image, View, TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    paddingTop: 40,
  },
  title: {
    fontSize: 24,
    textAlign: 'center',
  },
  profilePhoto: {
    width: 140,
    height: 140,
    borderRadius: 70,
    marginVertical: 20,
  },
  middleView: {
    flex: 1,
    alignItems: 'center',
  },
  button: {
    backgroundColor: '#2980b9',
    paddingVertical: 10,
    paddingHorizontal: 15,
  },
  buttonText: {
    color: 'white',
    fontSize: 16,
  },
  row: {
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  tabItem: {
    flex: 1,
    textAlign: 'center',
  },
});

const App = () => (
  <View style={styles.container}>
    <Text style={styles.title}>
      {'Welcome to the demo of\n"React Native Copilot"'}
    </Text>
    <View style={styles.middleView}>
       <Image
         source={{ uri: 'https://pbs.twimg.com/profile_images/527584017189982208/l3wwN-l-_400x400.jpeg' }}
         style={styles.profilePhoto}
       />
      <TouchableOpacity style={styles.button}>
        <Text style={styles.buttonText}>START THE TUTORIAL!</Text>
      </TouchableOpacity>
    </View>
    <View style={styles.row}>
      <Text style={styles.tabItem}>
        <Ionicons name="ios-contact" size={40} color="#888" />
      </Text>

      <Ionicons style={styles.tabItem} name="ios-game-controller-b" size={40} color="#888" />
      <Ionicons style={styles.tabItem} name="ios-globe" size={40} color="#888" />
      <Ionicons style={styles.tabItem} name="ios-navigate-outline" size={40} color="#888" />
      <Ionicons style={styles.tabItem} name="ios-rainy" size={40} color="#888" />
    </View>
  </View>
);

export default App;

Now let’s use react-native-copilot to add the walk-through to the screen. First, we should apply the copilot() higher order component to the main component:

import { copilot } from 'react-native-copilot';

const App = () => // ...

export default copilot()(App);

It will wrap the App component within a View element which will later be used to add the walk-through components to the screen. In addition to, it will inject the state of the walk-through to the child context.

Now we need to make a walkthroughable component that would be used as one of the copilot steps. A walkthroughable component is a component that accepts an object named copilot as a prop, that contains two functions, onLayout and ref. These functions are used to keep track of the elements positions in order to make the walk-through modals the right size and be in the right position. In most cases, these functions can simply be passed directly to the native elements, however, they also can be invoked manually with some arbitrary algorithms as well if needed.

The easiest way to make a walkthroughable component for the native components would be using the walkthroughable higher-order component:

import { walkthroughable } from 'react-native-copilot';

const WalkthroughableText = walkthroughable(Text);
const WalkthroughableImage = walkthroughable(Image);

Now we can replace some Text and Image components with WalkthroughableText and WalkthroughableImage. The next step would be to wrap the elements that we want to have a walk-through step for inside a CopilotStep element. The CopilotStep component accepts these as props:

name: A unique name for the walk-through step. order: A positive number indicating the order of the step in the entire walk-through. text: The text shown as the description for the step.

Also, the copilot() higher-order component, injects some essential props to the main component:

start: A function that you can invoke to start the walk-through. currentStep: An object of the currentStep’s information. visible: True, if the walk-through modal is visible. Let’s make the button onPress event trigger the walk-through:

<TouchableOpacity style={styles.button} onPress={() => props.start()}>
  <Text style={styles.buttonText}>START THE TUTORIAL!</Text>
</TouchableOpacity>

Now it’s time turn our screen into a completely walkthroughable one:

import React from 'react';
import { StyleSheet, Text, Image, View, TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons';

import { copilot, walkthroughable, CopilotStep } from 'react-native-copilot';

const WalkthroughableText = walkthroughable(Text);
const WalkthroughableImage = walkthroughable(Image);

/* const styles = ... */

const App = (props) => (
  <View style={styles.container}>
    <CopilotStep text="Hey! This is the first step of the tour!" order={1} name="openApp">
      <WalkthroughableText style={styles.title}>
        {'Welcome to the demo of\n"React Native Joyride"'}
      </WalkthroughableText>
    </CopilotStep>
    <View style={styles.middleView}>
      <CopilotStep text="Here goes your profile picture!" order={2} name="secondText">
        <WalkthroughableImage
          source={{ uri: 'https://pbs.twimg.com/profile_images/527584017189982208/l3wwN-l-_400x400.jpeg' }}
          style={styles.profilePhoto}
        />
      </CopilotStep>
      <TouchableOpacity style={styles.button} onPress={() => props.start()}>
        <Text style={styles.buttonText}>START THE TUTORIAL!</Text>
      </TouchableOpacity>
    </View>
    <View style={styles.row}>
      <CopilotStep text="Here is an item in the corner of the screen." order={3} name="thirdText">
        <WalkthroughableText style={styles.tabItem}>
          <Ionicons name="ios-contact" size={40} color="#888" />
        </WalkthroughableText>
      </CopilotStep>

      <Ionicons style={styles.tabItem} name="ios-game-controller-b" size={40} color="#888" />
      <Ionicons style={styles.tabItem} name="ios-globe" size={40} color="#888" />
      <Ionicons style={styles.tabItem} name="ios-navigate-outline" size={40} color="#888" />
      <Ionicons style={styles.tabItem} name="ios-rainy" size={40} color="#888" />
    </View>
  </View>
);

export default copilot()(App);

How does it work? 🤔

Everything is happening in the copilot higher-order component. The global state of the walk-through is stored and handled within this component. Whenever a CopilotStep component is mounted, it registers itself as a new step in the entire walk-through by calling the registerStep() method through the context. The position and the size of the component is always stored as a part of the step data. The child element inside the CopilotStep takes an additional copilot prop which is an object that has two methods: ref and onLayout. These two methods need to be applied to the outermost native component i.e. View, Image, Text, etc. which are needed in order to measure the component’s layout. Whenever the start function is invoked, it starts measuring the position and the sizes of the copilot step components.

How is the overlay mask created? 🧐

Copilot offers two types of overlays: svg and view. The first one provides a smoother animation but it requires react-native-svg. This mask is rendered using a path svg element whose path describes a rectangle within a larger rectangle that fills the entire screen. You may ask why we didn’t use ART for that. We didn’t because it doesn’t support fill-rules. But what is fill-rule and why do we need it for drawing the mask?

fill-rule essentially describes how the interior of the shape should be painted. fill-rule: evenodd allows us to paint only the outer rectangle leaving the inner one empty. It determines the “insideness” of a point is determined by drawing a trial ray from it to some random point in infinity. If the number of path segments that it crosses along the way is even it is considered as “outside” otherwise it is “inside”. Here’s an example svg similar to what we use for the copilot’s mask.

Conclusion

We hope that now you can easily create a step-by-step walk-through for any of your React Native apps. If you have questions, problems, or simply wish to let us know what you love most about the release, please let us know via Twitter or on Github!

Let's stay connected. Join our monthly newsletter to receive updates on events, training, and helpful articles from our team.