Realtime Geolocation Tracking with React Native

cameron_akhavan

Cameron Akhavan

Posted on July 15, 2019

Realtime Geolocation Tracking with React Native

At PubNub, we believe in powering the global frontier of connected experiences. Whether it be the burgeoning paradigm of IoT or the relentless expansion of online applications, our mission extends to Internet technologies of all shapes and sizes.

With our fully supported React SDK, developers now have the freedom of realtime web-app development at their fingertips.

In this article, we will show you just how easy it is to build realtime geolocation tracking with one of the most popular mobile app frameworks. Whether you want to build a rideshare app, on-demand delivery service, or a Pokemon Go game, this article aims to give you all the necessary tools and skills to get you on your way!

Today, we will be building a simple geotracking app that renders multiple users on a map view. Users will be able to toggle their location permissions as well as click a button to zoom the map on their location. And of course, it wouldn't be PubNub if we didn't include PubNub Presence to track how many users are currently online in the app.

Before jumping right into the code, be sure you sign up for a free PubNub account so we don't run into any issues later.

Step 1: Setting Up Your Environment

In this section, we are going to install the necessary tools and dependencies to be able to simulate, run, and test our react-native application.

The first tool you'll need to add to your set is Xcode. For Mac users, you can simply download Xcode for free in the app store. For PC users, you will need to simulate Mac OS with a Virtual Machine if you want to develop your app for iPhone. You can see how to do this here.

The next tool we're going to install is Android Studio. This will allow you to develop your app for Android.

Next, you're going to install the heart and soul of our app: React Native. This is an open-source platform developed by Facebook that has become very popular over the years. React Native allows developers to write their applications in one language across multiple platforms, which will make our job a lot easier for developing for iOS and Android.

To set up our React Native development environment, we'll be using the React Native CLI, which will allow us to quickly install libraries, link packages, and simulate our app.

Assuming that you have Node 10+ installed, you can use npm to install the React Native CLI command line utility:



npm install -g react-native-cli
npm install -g react-native


Enter fullscreen mode Exit fullscreen mode

Then run the following commands to create a new React Native project called "YourProject":



react-native init YourProject
cd YourProject


Enter fullscreen mode Exit fullscreen mode

To see if everything's working properly, run this command to test your app with Xcode's iPhone simulator:



react-native run-ios


Enter fullscreen mode Exit fullscreen mode

Step 2: Installing and Linking Libraries

Now we are going to install the libraries we are going to use, then link them to our React Native app.

The first library we are going to import and link is PubNub's React SDK for handling data streaming. In your project directory, install the library with:



npm install --save pubnub pubnub-react


Enter fullscreen mode Exit fullscreen mode

and link the library with:



react-native link pubnub-react


Enter fullscreen mode Exit fullscreen mode

Next, we'll need the react-native-maps library made by Airbnb for our interactive map API. Install the library just as before:



npm install react-native-maps --save


Enter fullscreen mode Exit fullscreen mode

and link the library with:



react-native link react-native-maps


Enter fullscreen mode Exit fullscreen mode

Lastly, we will install the react-native-responsive API, which will make styling our components easier:



npm install --save react-native-responsive-screen


Enter fullscreen mode Exit fullscreen mode

and then link:



react-native link react-native-responsive-screen


Enter fullscreen mode Exit fullscreen mode

Step 3: Building the App

Now it's time to finally start building our app in React Native!

Importing Libraries

Open up your App.js file and import the libraries we installed earlier along with some basic React-Native components.



import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View, TouchableOpacity, Switch, Image} from 'react-native';
import {widthPercentageToDP as wp, heightPercentageToDP as hp} from 'react-native-responsive-screen';
import MapView, {Marker} from 'react-native-maps';
import PubNubReact from 'pubnub-react';


Enter fullscreen mode Exit fullscreen mode

Constructor and State Variables

In the same App.js file initialize a constructor to pass in our props as well as initialize a PubNub instance.



constructor(props) {
  super(props);

  this.pubnub = new PubNubReact({
    publishKey: "YOUR PUBLISH KEY",
    subscribeKey: "YOUR SUBSCRIBE KEY"
  });

  this.pubnub.init(this);
}


Enter fullscreen mode Exit fullscreen mode

As you can see, we first declare a PubNub instance variable with the Publish and Subscribe keys we were given when signing up for a free PubNub account. Then we initialized the PubNub instance at the end of the constructor.

NOTE: It is very important that the PubNub instance is initialized at the end of the constructor as the code will not work if done otherwise.

Now let's create some state variables that we'll need while our app is running. If you haven't used state in React yet, it may be useful to read up about state in React Native before moving on.



constructor(props) {
  super(props);

  this.pubnub = new PubNubReact({
    publishKey: "YOUR PUBLISH KEY",
    subscribeKey: "YOUR SUBSCRIBE KEY"
  });

  //Base State
  this.state = {
    currentLoc: { //Track user's current location
      latitude: -1,
      longitude: -1
    },
    numUsers: 0, //track number of users on the app
    username: "A Naughty Moose", //user's username
    fixedOnUUID: "",
    focusOnMe: false, //zoom map to user's current location if true
    users: new Map(), //store data of each user in a Map
    isFocused: false, 
    allowGPS: true, //toggle the app's ability to gather GPS data of the user
  };

  this.pubnub.init(this);
}


Enter fullscreen mode Exit fullscreen mode

While most the state variables seem fairly intuitive for a geotracking app, the users map requires further explanation.

The users map will facilitate how we render multiple users on our app. Each entry in the map will represent one user and will map to the specific in-app data that user contains (GPS coordinates, UUID, allowGPS, etc). We will then use PubNub to publish JSON data updates from each user to update the mapping and re-render the application's state variables accordingly. You will see this in action in the later sections.

For example, if we want to update a user's allowGPS variable, we publish a JSON object to update that user's variable mapping:



this.pubnub.publish({
  message: {
    hideUser: true
  },
  channel: "channel"
});


Enter fullscreen mode Exit fullscreen mode

PubNub

First, declare an asynchronous function in the ComponentDidMount().



async componentDidMount() {
  this.setUpApp()
}


Enter fullscreen mode Exit fullscreen mode

In order to start receiving PubNub messages within our app, we must declare a PubNub event listener followed by a PubNub subscriber callback, specifying the channel like so:



async setUpApp(){

    this.pubnub.getMessage("YOUR CHANNEL", msg => {

/*------------WE'LL IMPLEMENT THIS LATER------------*/

    });

    this.pubnub.subscribe({
      channels: ["YOUR CHANNEL"],
    });
  }


Enter fullscreen mode Exit fullscreen mode

We'll further implement this function as we later build upon our app.

React Native Maps

We will now start implementing the interactive map for our users as well as track their GPS data.

To collect a user's position, we implement the react-native-maps watchPosition() function below our PubNub subscriber:



//Track motional Coordinates
navigator.geolocation.watchPosition(
  position => {
    this.setState({
      currentLoc: position.coords
    });
    if (this.state.allowGPS) {
      this.pubnub.publish({
        message: {
          latitude: position.coords.latitude,
          longitude: position.coords.longitude,
        },
        channel: "channel"
      });
    }
    //console.log(positon.coords);
  },
  error => console.log("Maps Error: ", error),
  {
    enableHighAccuracy: true,
    distanceFilter: 100 //grab the location whenever the user's location changes by 100 meters
  }
);


Enter fullscreen mode Exit fullscreen mode

You should now begin to see the reasoning for our user map framework. After we collect the position coordinates, we publish the latitude and longitude data to the channel. The channel will later update this user's location data based on the publisher's UUID.

Now just like any other map application, we should add a function to center the map on the user's location if a button is pressed. To add this functionality, add this function implementation.



focusLoc = () => {
   if (this.state.focusOnMe || this.state.fixedOnUUID) {
     this.setState({
       focusOnMe: false,
       fixedOnUUID: ""
     });
   } else {
     region = {
       latitude: this.state.currentLoc.latitude,
       longitude: this.state.currentLoc.longitude,
       latitudeDelta: 0.01,
       longitudeDelta: 0.01
     };
     this.setState({
       focusOnMe: true
     });
     this.map.animateToRegion(region, 2000);
   }
 }


Enter fullscreen mode Exit fullscreen mode

When called, this function will center the map's viewing region on the user's current location.

Lastly, if we want the user to have the ability to turn off their GPS location, we need to toggle the allowGPS state. To do this, also add this function to your code.



toggleGPS = () => {
   this.setState({
     allowGPS: !this.state.allowGPS
   });
 };


Enter fullscreen mode Exit fullscreen mode

User Data Aggregation

Now, let's go back to the PubNub event listener we defined earlier. The purpose of the event listener for this app is to take the data updates published to our channel and update the state variables of our app accordingly.

In order to update the users map, we'll first initialize a copy of the mapping to manipulate:



this.pubnub.getMessage("channel", msg => {
  let users = this.state.users;
});


Enter fullscreen mode Exit fullscreen mode

We then check if the incoming message is a request from a user to hide their GPS data and remove them from the mapping accordingly.



if (msg.message.hideUser) {
  users.delete(msg.publisher);
  this.setState({
    users
  });
}else{
/*something else*/
}


Enter fullscreen mode Exit fullscreen mode

Otherwise, the message contains data updates for a user and we must declare a new user (with the updated values) to replace the old one.



else{
        coord = [msg.message.latitude, msg.message.longitude]; //Format GPS Coordinates for Payload

        let oldUser = this.state.users.get(msg.publisher);

        let newUser = {
          uuid: msg.publisher,
          latitude: msg.message.latitude,
          longitude: msg.message.longitude,
        };

        if(msg.message.message){
          Timeout.set(msg.publisher, this.clearMessage, 5000, msg.publisher);
          newUser.message = msg.message.message;
        }else if(oldUser){
          newUser.message = oldUser.message
        }
        users.set(newUser.uuid, newUser);

        this.setState({
          users
        });
}


Enter fullscreen mode Exit fullscreen mode

We just implemented the receiving end of updating a user's data. Now we shall implement the sending end where the user will actually publish messages with their data updates.

In order to know when a user has changed one of their data variables, we must use an event handler to detect these changes. For this we will use React's componentDidUpdate() function, which will trigger anytime there is a change in the app's data.

We first specify the componentDidUpdate() function to pass in the previous props and state.



componentDidUpdate(prevProps, prevState) {

}


Enter fullscreen mode Exit fullscreen mode

Within this function, we then check whether the user has toggled their allowGPS and focusOnMe variables and make the necessary changes to the app's function and state.



if (prevState.allowGPS != this.state.allowGPS) { //check whether the user just toggled their GPS settings
  if (this.state.allowGPS) { //if user toggled to show their GPS data, we add them to the user Map once again
    if (this.state.focusOnMe) { //if user toggled to focus map view on themselves
      this.animateToCurrent(this.state.currentLoc, 1000);
    }
    let users = this.state.users; //make a copy of the users array to manipulate

    //create a new user object with updated user values to replace the old user
    let tempUser = {
      uuid: this.pubnub.getUUID(),
      latitude: this.state.currentLoc.latitude,
      longitude: this.state.currentLoc.longitude,
      image: this.state.currentPicture,
      username: this.state.username
    };
    users.set(tempUser.uuid, tempUser);
    this.setState( //quickly update the user Map locally
      {
        users
      },
      () => {
        this.pubnub.publish({ //publish updated user to update everyone's user Map
          message: tempUser,
          channel: "channel"
        });
      }
    );
  } else { //if user toggled to hide their GPS data
    let users = this.state.users;
    let uuid = this.pubnub.getUUID();

    users.delete(uuid); //delete this user from the user Map
    this.setState({ //update the userMap
      users,
    });
    this.pubnub.publish({ //let everyone else's user Map know this user wants to be hidden
      message: {
        hideUser: true
      },
      channel: "channel"
    });
  }
}


Enter fullscreen mode Exit fullscreen mode

For those of you who are paying close attention to what we are doing here, you may have noticed a redundancy in this code snippet. Why are we setting the state of the updated user locally and then publishing the updated user object to the channel? Isn't that setting the state twice?

While the assumption is correct, there is a method to the madness. We first update the state locally so we can update the user's screen as fast as possible. Then we publish the updated user object to the channel so that everyone else on the network can update their state as well.

Rendering

It's time to watch our realtime geotracking app beautifully come together! We'll now be working in the render() function of our App.js file. As we incrementally build upon components in this section, be sure to look out for these comments:



/*-----Next Snippet Goes Here-----*/


Enter fullscreen mode Exit fullscreen mode

...as they will direct you where to insert the next piece of code.

Let's first declare our user map into a useable array



let usersArray = Array.from(this.state.users.values());


Enter fullscreen mode Exit fullscreen mode

Now inside your return, render your map component from React-Native-Maps by setting the initial region to whatever coordinates you want.



return (
     <View style={styles.container}  >
          <MapView
            style={styles.map}
            ref={ref => (this.map = ref)}
            onMoveShouldSetResponder={this.draggedMap}
            initialRegion={{
              latitude: 36.81808,
              longitude: -98.640297,
              latitudeDelta: 60.0001,
              longitudeDelta: 60.0001
            }}
          >
/*-----Next Snippet Goes Here-----*/          
          </MapView>
     </View>
);


Enter fullscreen mode Exit fullscreen mode

We shall now iterate through our map to begin rendering every user on our network.



{usersArray.map((item) => (/*------Next Snippet Goes Here-------*/))}


Enter fullscreen mode Exit fullscreen mode

For each user, we must render a marker component from React-Native-Maps as well as an image to represent that user.



<Marker
  style={styles.marker}
  key={item.uuid} //distinguish each user's marker by their UUID
  coordinate={{ //user's coordinates 
    latitude: item.latitude,
    longitude: item.longitude
  }}
  ref={marker => {
    this.marker = marker;
  }}
>
  <Image
      style={styles.profile}
      source={require('./LOCATION OF YOUR USER IMAGE PROFILES')} //User's image 
  />
</Marker>


Enter fullscreen mode Exit fullscreen mode

Below the MapView, we can define a toggle switch for the user to toggle their allowGPS state



<View style={styles.topBar}>
  <View style={styles.rightBar}>
      <Switch
      value={this.state.allowGPS}
      style={styles.locationSwitch}
      onValueChange={this.toggleGPS}
      />
  </View>
</View>


Enter fullscreen mode Exit fullscreen mode

Lastly, we can add a button to center the map on the user.



<View style={styles.bottom}>
<View style={styles.bottomRow}>   
  <TouchableOpacity onPress={this.focusLoc}>
    <Image style={styles.focusLoc} source={require('./heart.png')} />
  </TouchableOpacity>
</View>
</View>


Enter fullscreen mode Exit fullscreen mode

Styling

Of course, you may style your components however you like, but here's a simple template I used for this project:



const styles = StyleSheet.create({
  bottomRow:{
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center"
  },
  marker: {
    justifyContent: "center",
    alignItems: "center",
    marginTop: Platform.OS === "android" ? 100 : 0,
  },
  topBar: {
    top: Platform.OS === "android" ? hp('2%') : hp('5%'),

    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center",
    marginHorizontal: wp("2%"),
  },
  rightBar: {
    flexDirection: "row",
    justifyContent: "flex-end",
    alignItems: "center"
  },
  leftBar: {
    flexDirection: "row",
    justifyContent: "flex-start",
    alignItems: "center"
  },
  locationSwitch: {
    left: 300,
  },
  container: {
    flex: 1
  },
  bottom: {
    position: "absolute",
    flexDirection:'column',
    bottom: 0,
    justifyContent: "center",
    alignSelf: "center",
    width: "100%",
    marginBottom: hp("4%"),
  },
  focusLoc: {
    width: hp("4.5%"),
    height: hp("4.5%"),
    marginRight: wp("2%"),
    left: 15
  },
  userCount: {
    marginHorizontal: 10
  },
  map: {
    ...StyleSheet.absoluteFillObject
  },
  profile: {
    width: hp("4.5%"),
    height: hp("4.5%")
  },
});


Enter fullscreen mode Exit fullscreen mode

Android Compatibility

For Android OS compatibility, place this snippet at the beginning of your setUpApp() function:



let granted;

if (Platform.OS === "android"){
  granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION ,
    {
      title: 'Location Permission',
      message:
        'PubMoji needs to access your location',
      buttonNegative: 'No',
      buttonPositive: 'Yes',
    });      
}


Enter fullscreen mode Exit fullscreen mode

then place an if() statement around the watchPosition() function like so



if (granted === PermissionsAndroid.RESULTS.GRANTED || Platform.OS === "ios") { /-----watchPosition()----/ }
else {
console.log( "ACCESS_FINE_LOCATION permission denied" )
}

Enter fullscreen mode Exit fullscreen mode




Step 4: Testing the App

When you're all good and ready you can simulate your app with either:

simulate IOS



react-native run-ios

Enter fullscreen mode Exit fullscreen mode




or for Android




react-native run-android

Enter fullscreen mode Exit fullscreen mode




Conclusion

Congratulations! You have created your very own realtime geotracking app in React Native! Feel free to send us any of your questions, concerns, or comments at devrel@pubnub.com.

If you're still hungry for more PubNub React Native content, here are some other articles that you may be interested in:

💖 💪 🙅 🚩
cameron_akhavan
Cameron Akhavan

Posted on July 15, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related