Realtime Geolocation Tracking with React Native
Cameron Akhavan
Posted on July 15, 2019
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
Then run the following commands to create a new React Native project called "YourProject":
react-native init YourProject
cd YourProject
To see if everything's working properly, run this command to test your app with Xcode's iPhone simulator:
react-native run-ios
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
and link the library with:
react-native link pubnub-react
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
and link the library with:
react-native link react-native-maps
Lastly, we will install the react-native-responsive API, which will make styling our components easier:
npm install --save react-native-responsive-screen
and then link:
react-native link react-native-responsive-screen
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';
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);
}
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);
}
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"
});
PubNub
First, declare an asynchronous function in the ComponentDidMount().
async componentDidMount() {
this.setUpApp()
}
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"],
});
}
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
}
);
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);
}
}
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
});
};
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;
});
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*/
}
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
});
}
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) {
}
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"
});
}
}
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-----*/
...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());
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>
);
We shall now iterate through our map to begin rendering every user on our network.
{usersArray.map((item) => (/*------Next Snippet Goes Here-------*/))}
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>
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>
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>
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%")
},
});
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',
});
}
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" )
}
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
or for Android
react-native run-android
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:
Posted on July 15, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.