DEV Community

Cover image for React Native Maps: How to install and use the map library on iOS and Android [2020]
Iván García for Reboot Studio

Posted on • Originally published at reboot.studio

React Native Maps: How to install and use the map library on iOS and Android [2020]

Maps have become one of the most popular interfaces for many of the applications that we have installed on our phones. Learning to work on maps, represent information appropriately and create a good navigation interface is becoming increasingly important.

In this post we will see how to integrate Google Maps into a React Native app using the react-native-maps library for iOS and Android. To develop an example as realistic as possible, we will recreate an Uber-style interface using a Bottom Sheet.

At the end of the post we will be able to develop an application like this one here.

react-native-maps-app-1

Project creationgoogle-cloud-project-1

For this project we are going to use Expo to speed up the installation process and make it easy for anyone who wants to download the repository to test the application. If you still do not have expo installed you can follow the official installation guide.

The first thing we will do is create a blank project using the expo cli.

#We create a project named google-maps-example. We select the "blank" template
$ expo init google-maps-example

$ cd google-maps-example

$ expo start
Enter fullscreen mode Exit fullscreen mode

Install react-native-maps library with Google Maps

Once the project is created, the next step is to add the react-native-map library using the command below.

expo install react-native-maps
Enter fullscreen mode Exit fullscreen mode

If you are not using expo in your project, you can use this command

npm install react-native-maps --save-exact

o

yarn add react-native-maps -E
Enter fullscreen mode Exit fullscreen mode

The difference between the first command and the second is that using the Expo cli we make sure to use the latest version of the library compatible with Expo.

It is worth mentioning that we can use the react-native-maps library with both Apple Maps and Google Maps. In this tutorial we will focus on using Google Maps as a map provider, but the steps to integrate Apple Maps are very similar.

Get the Google Maps API Key

In order to use Google Maps in our application, it is necessary to enable the iOS and Android SDK in a Google project with an active billing account in the Google Cloud Console and generate an API key to add it to our codebase.

Let's see step by step how to obtain the Google Maps API Key.

  1. The first thing we will do is going to Google Cloud Console and create a new project that we will name google-maps-example-reboot.
    google-cloud-project-1

  2. Once we have created our project, we need to enable the Maps SDK for Android and the Maps SDK for iOS within the apis and services library.
    google-cloud-library-1
    sdk-google-maps-ios-1
    sdk-google-maps-android-1

  3. Once the sdks are enabled, we need to create an API key. For this we go to the Control Panel → Create Credentials → API Key
    admin-panel-google-cloud-1
    admin-panel-google-cloud-api-key-1
    create-api-key-google-cloud-2

  4. Once the API key is created, it is highly recommended to limit it to the libraries that we want to use and to the applications that will have permission to use it using the application's fingerprint and the identifier bundle.
    limit-api-google-cloud-2

Now we have the API Key that we need to add to our application. Depending on whether we are using expo or a bare project, the way to do it will change.

Add API Key on Expo

On Expo we simply go to app.json and add this snippet:

// app.json

{
  "expo": {
    "name": "google-maps-example",
    "slug": "google-maps-example",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./src/assets/icon.png",
    "splash": {
      "image": "./src/assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "updates": {
      "fallbackToCacheTimeout": 0
    },
    "assetBundlePatterns": [
      "**/*"
    ],
    "ios": {
      "supportsTablet": true,
      "config": {
          "googleMapsApiKey": "REPLACE_FOR_API_KEY"
      }
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./src/assets/adaptive-icon.png",
        "backgroundColor": "#FFFFFF"
      },
      "config": {
        "googleMaps": {
          "apiKey": "REPLACE_FOR_API_KEY"
        }
      }
    },
    "web": {
      "favicon": "./src/assets/favicon.png"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Add API Key on Android

If it is a Bare Android project it will be necessary to add the API Key in google_maps_api.xml in the path android/app/src/main/res/values.

<resources>
  <string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">(api key here)</string>
</resources>
Enter fullscreen mode Exit fullscreen mode

Add API Key on iOS

On iOS you need to edit AppDelegate.m file to include the following snippet.

+ #import <GoogleMaps/GoogleMaps.h>
@implementation AppDelegate
...
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
+ [GMSServices provideAPIKey:@"_YOUR_API_KEY_"]; // add this line using the api key obtained from Google Console
...
Enter fullscreen mode Exit fullscreen mode
  # React Native Maps dependencies
  rn_maps_path = '../node_modules/react-native-maps'
  pod 'react-native-google-maps', :path => rn_maps_path
  pod 'GoogleMaps'
  pod 'Google-Maps-iOS-Utils'
Enter fullscreen mode Exit fullscreen mode

It is important to note that when using location permissions you must tell Apple why you need to access the user's location, otherwise Apple will reject your application when you upload it to the App Store. This can be done in the Info.plist file by editing the NSLocationWhenInUseUsageDescription field explaining clearly and concisely why you need to know the location.

Add and customize a map in React Native

Now that we have integrated the map library, we are going to start by creating a screen with the map visualization and customizing the style with the different options it provides. For this we are going to create a Map.js component like the following.

import React from 'react';
import { StyleSheet, View, Dimensions } from 'react-native';
import MapView, { PROVIDER_GOOGLE } from 'react-native-maps';
import { mapStyle } from './mapStyle';

export function MapScreen() {
  return (
    <View style={styles.container}>
      <MapView
        customMapStyle={mapStyle}
        provider={PROVIDER_GOOGLE}
        style={styles.mapStyle}
        initialRegion={{
          latitude: 41.3995345,
          longitude: 2.1909796,
          latitudeDelta: 0.003,
          longitudeDelta: 0.003,
        }}
        mapType="standard"
      ></MapView>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'black',
    alignItems: 'center',
    justifyContent: 'center',
  },
  mapStyle: {
    width: Dimensions.get('window').width,
    height: Dimensions.get('window').height,
  },
});
Enter fullscreen mode Exit fullscreen mode

As we can see, the main component is MapView that has multiple props to customize its behavior. In this case, the most important ones are provider where we indicate that we want to use Google Maps, initialRegion which will be the initial location, mapType where we can define the type of map that is loaded and finally customMapStyle where we will set the custom style of the map we want to use.

react-native-maps-simulator-3

If we look at the Google's official documentation we see that we can customize almost all the elements of the map. In this case we seek to make a minimalist interface so we will use the following styles.

//mapStyle.js
export const mapStyle = [
  {
    featureType: 'water',
    elementType: 'geometry',
    stylers: [
      {
        color: '#e9e9e9',
      },
      {
        lightness: 17,
      },
    ],
  },
  {
    featureType: 'landscape',
    elementType: 'geometry',
    stylers: [
      {
        color: '#f5f5f5',
      },
      {
        lightness: 20,
      },
    ],
  },
  {
    featureType: 'road.highway',
    elementType: 'geometry.fill',
    stylers: [
      {
        color: '#ffffff',
      },
      {
        lightness: 17,
      },
    ],
  },
  {
    featureType: 'road.highway',
    elementType: 'geometry.stroke',
    stylers: [
      {
        color: '#ffffff',
      },
      {
        lightness: 29,
      },
      {
        weight: 0.2,
      },
    ],
  },
  {
    featureType: 'road.arterial',
    elementType: 'geometry',
    stylers: [
      {
        color: '#ffffff',
      },
      {
        lightness: 18,
      },
    ],
  },
  {
    featureType: 'road.local',
    elementType: 'geometry',
    stylers: [
      {
        color: '#ffffff',
      },
      {
        lightness: 16,
      },
    ],
  },
  {
    featureType: 'poi',
    elementType: 'geometry',
    stylers: [
      {
        color: '#f5f5f5',
      },
      {
        lightness: 21,
      },
    ],
  },
  {
    featureType: 'poi.park',
    elementType: 'geometry',
    stylers: [
      {
        color: '#dedede',
      },
      {
        lightness: 21,
      },
    ],
  },
  {
    elementType: 'labels.text.stroke',
    stylers: [
      {
        visibility: 'on',
      },
      {
        color: '#ffffff',
      },
      {
        lightness: 16,
      },
    ],
  },
  {
    elementType: 'labels.text.fill',
    stylers: [
      {
        saturation: 36,
      },
      {
        color: '#333333',
      },
      {
        lightness: 40,
      },
    ],
  },
  {
    elementType: 'labels.icon',
    stylers: [
      {
        visibility: 'off',
      },
    ],
  },
  {
    featureType: 'transit',
    elementType: 'geometry',
    stylers: [
      {
        color: '#f2f2f2',
      },
      {
        lightness: 19,
      },
    ],
  },
  {
    featureType: 'administrative',
    elementType: 'geometry.fill',
    stylers: [
      {
        color: '#fefefe',
      },
      {
        lightness: 20,
      },
    ],
  },
  {
    featureType: 'administrative',
    elementType: 'geometry.stroke',
    stylers: [
      {
        color: '#fefefe',
      },
      {
        lightness: 17,
      },
      {
        weight: 1.2,
      },
    ],
  },
];
Enter fullscreen mode Exit fullscreen mode

Customizing a Google map can be tedious, that's why there are websites like Snazzymaps that gather templates with different styles that we can directly copy their attributes and use as a template.

react-native-maps-simulator-2-1

Add Markers to Google Maps in React Native

The next thing we will do is to add markers to our map. To do this we will create a constant MARKERS_DATA with the following structure.

import { default as Reboot } from '../assets/reboot.png';
import { default as Cravy } from '../assets/cravy.png';
import { default as Dribbble } from '../assets/dribbble.png';
import { default as Basecamp } from '../assets/basecamp.png';
import { default as Discord } from '../assets/discord.png';
import { default as OnePassword } from '../assets/onepassword.png';

export const MARKERS_DATA = [
  {
    id: '1',
    latitude: 41.3997999,
    longitude: 2.1909796,
    color: '#2F3136',
    name: 'Reboot Studio',
    direction: 'Carrer de Pujades, 100',
    img: Reboot,
  },
  {
    id: '2',
    latitude: 41.3995445,
    longitude: 2.1915268,
    color: '#A3EAD8',
    name: 'Cravy',
    direction: 'Carrer de Pujades, 101',
    img: Cravy,
  },
  {
    id: '3',
    latitude: 41.4009999,
    longitude: 2.1919999,
    color: '#E990BB',
    name: 'Dribbble',
    direction: 'Carrer de Pujades, 102',
    img: Dribbble,
  },
  {
    id: '4',
    latitude: 41.4001999,
    longitude: 2.1900096,
    color: '#EFD080',
    name: 'Basecamp',
    direction: 'Carrer de Pujades, 103',
    img: Basecamp,
  },
  {
    id: '5',
    latitude: 41.40009,
    longitude: 2.1909796,
    color: '#98AFE9',
    name: 'Discord',
    direction: 'Carrer de Pujades, 104',
    img: Discord,
  },
  {
    id: '6',
    latitude: 41.4009999,
    longitude: 2.1909796,
    color: '#4E87EB',
    name: '1 Password',
    direction: 'Carrer de Pujades, 105',
    img: OnePassword,
  },
];
Enter fullscreen mode Exit fullscreen mode

Once we have our data ready, we can add it to the map by importing the library's Marker component within MapView. To do this we will use an Array.map function with the MARKERS_DATA that we have created.

//Map.js
import React from 'react';
import { StyleSheet, View, Dimensions } from 'react-native';
import MapView, { PROVIDER_GOOGLE, Marker } from 'react-native-maps';
import { mapStyle } from './mapStyle';
import { MARKERS_DATA } from '../../data';

export function MapScreen() {
  return (
    <View style={styles.container}>
      <MapView
        customMapStyle={mapStyle}
        provider={PROVIDER_GOOGLE}
        style={styles.mapStyle}
        initialRegion={{
          latitude: 41.3995345,
          longitude: 2.1909796,
          latitudeDelta: 0.003,
          longitudeDelta: 0.003,
        }}
        mapType="standard"
      >
        {MARKERS_DATA.map((marker) => (
          <Marker
            key={marker.id}
            coordinate={{
              latitude: marker.latitude,
              longitude: marker.longitude,
            }}
          ></Marker>
        ))}
      </MapView>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'black',
    alignItems: 'center',
    justifyContent: 'center',
  },
  mapStyle: {
    width: Dimensions.get('window').width,
    height: Dimensions.get('window').height,
  },
});
Enter fullscreen mode Exit fullscreen mode

Voilà! We already have our markers on the map. But it still looks like any standard Google Maps map, so in the next step we're going to give it some personality by customizing the style of the markers.

react-native-markers-1

Customize Google Maps Markers in React Native

The react-native-maps library includes several props to customize the style of the markers, but the best option if you want to create completely customized markers is to use the Marker component as a wrapper and create your own component with the style you want.

Following our minimalist interface we will add some circular markers and we will smoothly animate the size when the marker is selected.

We are going to create the CustomMarker component and a useMarkerAnimation hook to manage the interaction of the animation.

//Custom Marker
import React from 'react';
import { Marker } from 'react-native-maps';
import Animated from 'react-native-reanimated';
import { StyleSheet, View } from 'react-native';
import { useMarkerAnimation } from './useMarkerAnimation';

export function CustomMarker({
  id,
  selectedMarker,
  color,
  latitude,
  longitude,
}) {
  const scale = useMarkerAnimation({ id, selectedMarker });

  return (
    <Marker
      coordinate={{
        latitude: latitude,
        longitude: longitude,
      }}
    >
      <View style={styles.markerWrapper}>
        <Animated.View
          style={[
            styles.marker,
            {
              backgroundColor: color,
              transform: [{ scale: scale }],
            },
          ]}
        ></Animated.View>
      </View>
    </Marker>
  );
}

const styles = StyleSheet.create({
  markerWrapper: {
    height: 50,
    width: 50,
    alignItems: 'center',
    justifyContent: 'center',
  },
  marker: {
    height: 22,
    width: 22,
    borderRadius: 20,
    borderColor: 'white',
    borderWidth: 2,
  },
});
Enter fullscreen mode Exit fullscreen mode

To manage the animations we have added the Reanimated and Redash libraries.

//useMarkerAnimation
import { useState, useEffect } from 'react';
import Animated from 'react-native-reanimated';
import { useTimingTransition } from 'react-native-redash';

export function useMarkerAnimation({ id, selectedMarker }) {
  const [active, setActive] = useState(0);

  useEffect(() => {
    const isActive = id === selectedMarker ? 1 : 0;
    setActive(isActive);
  }, [id, selectedMarker]);

  const transition = useTimingTransition(active, {
    duration: 200,
  });

  const scale = Animated.interpolate(transition, {
    inputRange: [0, 1],
    outputRange: [1, 1.5],
  });

  return scale;
}
Enter fullscreen mode Exit fullscreen mode

Finally we replace the default Marker from the map screen with our custom marker we have just created.

//Map.js
import React from 'react';
import { StyleSheet, View, Dimensions } from 'react-native';
import MapView, { PROVIDER_GOOGLE } from 'react-native-maps';
import { CustomMarker } from '../../components';
import { MARKERS_DATA } from '../../data';
import { mapStyle } from './mapStyle';

export function MapScreen() {
  return (
    <View style={styles.container}>
      <MapView
        customMapStyle={mapStyle}
        provider={PROVIDER_GOOGLE}
        style={styles.mapStyle}
        initialRegion={{
          latitude: 41.3995345,
          longitude: 2.1909796,
          latitudeDelta: 0.003,
          longitudeDelta: 0.003,
        }}
        mapType="standard"
      >
        {MARKERS_DATA.map((marker) => (
          <CustomMarker
            key={marker.id}
            id={marker.id}
            selectedMarker={null}
            color={marker.color}
            latitude={marker.latitude}
            longitude={marker.longitude}
          ></CustomMarker>
        ))}
      </MapView>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'black',
    alignItems: 'center',
    justifyContent: 'center',
  },
  mapStyle: {
    width: Dimensions.get('window').width,
    height: Dimensions.get('window').height,
  },
});
Enter fullscreen mode Exit fullscreen mode

All right! We already have our custom markers in our map app. But there is still a step left: we need to be able to navigate between the different markers. To do this we will create an interface based on a Bottom Sheet similar to the one found in applications such as Uber or Google Maps. This component will allow us to manage navigation between markers.

react-native-custom-markers-1

Manage map navigation

Let's see how we can navigate the map using both the animateCamera and the animateToRegion function. For this we need to create a map reference to be able to use it and call these functions. In our case we have created a hook to manage this logic.

//useMap.js
import { useState, useRef, useCallback } from 'react';

const DEVIATION = 0.0002;

export function useMap() {
  const mapRef = useRef(null);
  const [selectedMarker, setSelectedMarker] = useState(null);

  const handleNavigateToPoint = useCallback(
    (id, lat, long) => {
      if (mapRef) {
        mapRef.current.animateCamera(
          {
            center: {
              latitude: lat - DEVIATION,
              longitude: long,
            },
            zoom: 18.5,
          },
          500
        );
      }
      setSelectedMarker(id);
    },
    [mapRef, setSelectedMarker]
  );

  const handelResetInitialPosition = useCallback(() => {
    if (mapRef) {
      mapRef.current.animateToRegion(
        {
          latitude: 41.3995345,
          longitude: 2.1909796,
          latitudeDelta: 0.003,
          longitudeDelta: 0.003,
        },
        500
      );
      setSelectedMarker(null);
    }
  }, [mapRef, setSelectedMarker]);

  return {
    mapRef,
    selectedMarker,
    handleNavigateToPoint,
    handelResetInitialPosition,
  };
}
Enter fullscreen mode Exit fullscreen mode

As we can see in the code above, the functions are quite simple. The animateCamera function receive as parameters: the center with the latitude and longitude, the Zoom and the time that the animation will take. In the case of animateToRegion function, the logic is very similar but instead of using the Type Camera it uses the Type Region.

In our case we have also added a setSelectedMarker to be able to enlarge the marker when the camera uses it as the center.

To use the hook we simply have to add it to our Map component. But before that we will create the component above the map to be able to use the hook functions.

We are going to create a Bottom Sheet component with the list of locations so when you click on one of these, the camera will move to that point and the selected marker will expand. For the component we have used the 'react-native-scroll-bottom-sheet' library that uses Reanimated to manage the component animations.

//BottomSheet.js
import React from 'react';
import { Dimensions, StyleSheet, View } from 'react-native';
import ScrollBottomSheet from 'react-native-scroll-bottom-sheet';
import { MARKERS_DATA } from '../../data';
import { ListItem } from './ListItem';

const windowHeight = Dimensions.get('window').height;

export function BottomSheet({ onPressElement }) {
  return (
    <ScrollBottomSheet
      componentType="FlatList"
      snapPoints={[100, '50%', windowHeight - 200]}
      initialSnapIndex={1}
      renderHandle={() => (
        <View style={styles.header}>
          <View style={styles.panelHandle} />
        </View>
      )}
      data={MARKERS_DATA}
      keyExtractor={(i) => i.id}
      renderItem={({ item }) => (
        <ListItem item={item} onPressElement={onPressElement} />
      )}
      contentContainerStyle={styles.contentContainerStyle}
    />
  );
}

const styles = StyleSheet.create({
  contentContainerStyle: {
    flex: 1,
    backgroundColor: 'white',
  },
  header: {
    alignItems: 'center',
    backgroundColor: 'white',
    paddingVertical: 20,
  },
  panelHandle: {
    width: 41,
    height: 4,
    backgroundColor: '#E1E1E1',
    borderRadius: 17,
  },
});
Enter fullscreen mode Exit fullscreen mode

We will also add a top menu that will allow us to reset the state of our map.

//TopBar.js
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { Avatar } from './Avatar';
import { RefreshButton } from './RefreshButton';

export function TopBar({ onPressElement }) {
  return (
    <View style={styles.container}>
      <Avatar />
      <RefreshButton onPressElement={onPressElement} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    position: 'absolute',
    left: 0,
    top: 40,
    width: '100%',
    zIndex: 1,
    flexDirection: 'row',
    justifyContent: 'space-between',
    paddingHorizontal: 10,
  },
});
Enter fullscreen mode Exit fullscreen mode

Finally the map component would look like this.

import React from 'react';
import { StyleSheet, View, Dimensions } from 'react-native';
import MapView, { PROVIDER_GOOGLE } from 'react-native-maps';
import { TopBar, BottomSheet, CustomMarker } from '../../components';
import { MARKERS_DATA } from '../../data';
import { useMap } from './useMap';
import { mapStyle } from './mapStyle';

export function MapScreen() {
  const {
    mapRef,
    selectedMarker,
    handleNavigateToPoint,
    handelResetInitialPosition,
  } = useMap();

  return (
    <View style={styles.container}>
      <TopBar onPressElement={handelResetInitialPosition} />
      <MapView
        ref={mapRef}
        customMapStyle={mapStyle}
        provider={PROVIDER_GOOGLE}
        style={styles.mapStyle}
        initialRegion={{
          latitude: 41.3995345,
          longitude: 2.1909796,
          latitudeDelta: 0.003,
          longitudeDelta: 0.003,
        }}
        mapType="standard"
      >
        {MARKERS_DATA.map((marker) => (
          <CustomMarker
            key={marker.id}
            id={marker.id}
            selectedMarker={selectedMarker}
            color={marker.color}
            latitude={marker.latitude}
            longitude={marker.longitude}
          ></CustomMarker>
        ))}
      </MapView>
      <BottomSheet onPressElement={handleNavigateToPoint} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'black',
    alignItems: 'center',
    justifyContent: 'center',
  },
  mapStyle: {
    width: Dimensions.get('window').width,
    height: Dimensions.get('window').height,
  },
});
Enter fullscreen mode Exit fullscreen mode

We have managed to build a maps application with a very simple interface that allows us to manage navigation between the different points of interest in a very intuitive way. Much more complex products can be built on top of this, but it is a good starting point if you are developing a map app in React Native in 2020.

react-native-maps-app-1

The complete project is available on GitHub so that you can download and work on it.

This post was originally published at Reboot Blog.

Top comments (0)