React Native で位置情報を取得して地図表示する

地図アプリで目的地までの経路を検索したり、場所を活用したゲームや SNS など、位置情報を利用したアプリは生活の中で当たり前になりました。
本記事では、React Native で位置情報を取得 / 追跡してアプリに地図表示する方法を、サンプルアプリを作りつつ紹介したいと思います。
※今回 React Native 公式の Geolocation を用いるので、 Expo についての内容は含みません🙏


# イントロダクション

## Ract Native 位置情報に必要な概念
- Geolocation
- [Location](https://docs.expo.io/versions/latest/sdk/location/) (Expoを使用する場合)

### Geolocation とは
React Native の Geolocation は、Webアプリケーションで位置情報を取得する際に利用される [Geolocation Web](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation) のアプリ拡張  API です。

Geolocation Web APIは、W3Cが仕様策定を進める規格であり、JavaScriptで位置情報を取得できるように標準化されています。
一般的なブラウザでサポートされており、 スマートフォンのようなGPS対応デバイス向けのウェブサイトだけではなく、 PC サイトなどでもユーザーの位置情報を利用したコンテンツを提供出来るようになっています。
Geolocation API において位置情報は、デバイスの GPS 機能の他に、IPアドレス、Wi-Fi などさまざまなソースを基に取得されます。また、Geolocation API から位置情報にアクセスする場合、ユーザーの許諾なしに位置情報を取得することは出来ません。

ちなみに、Geolocation API で取得できるのは緯度や経度といった情報であり、住所を取得することはできません。緯度/経度情報を住所に変換する場合には、[Google Geocoding API](https://developers.google.com/maps/documentation/geocoding/start) などを利用します。

React Native の Geolocation で提供される機能としては主に以下の2つになります。今回はこの2つの機能について、サンプルアプリを作ってみたいと思います。

- 現在位置情報の取得
- 継続的に位置情報を追跡して取得

#### React Native Community への移管

Geolocation は 2019年はじめに発足した React Native の [Lean Core 運動](https://github.com/facebook/react-native/issues/23313) の対象とされ、v0.60で React Native Community に機能が移管されました。
**v0.60以降 Geolocation は [react-native-geolocation](https://github.com/react-native-community/react-native-geolocation) から利用できます。**

*React Native から Geolocation が削除された commits*

- [Move iOS Geolocation code out from the repo · facebook/react-native@17dbf98](https://github.com/facebook/react-native/commit/17dbf988845bb7815dbb6182218c8c28d027fb91#diff-b41b57dec28619ad3fb93ee4466a0731)
- [Remove navigator.geolocation, use Geolocation · facebook/react-native@45bd2b5](https://github.com/facebook/react-native/commit/45bd2b514b581716ffb9e5fffe7500acb119cee8#diff-b41b57dec28619ad3fb93ee4466a0731)
- [Move Geolocation JS code to FB internal · facebook/react-native@9834c58](https://github.com/facebook/react-native/commit/9834c580af654366bf0d38b78cd2694b0a0c477f#diff-b41b57dec28619ad3fb93ee4466a0731)

v0.61 時点では、公式ドキュメントが更新がされていないようなので注意です。（[PR](https://github.com/facebook/react-native-website/pull/1239) は出されていたようなのですが）

v0.59 以下の React Native では Geolocation を使用する場合には `import` が不要で、`navigator.geolocation` でグローバルにアクセスできます。
react-native-geolocation をインストールして使用することもできますが、その場合は 0.60 とは違って手動で `react-native link` する必要があります。手順は [ドキュメント](https://github.com/react-native-community/react-native-geolocation#mostly-automatic-installation-react-native-059-and-lower)をご参照ください。
ですので、v0.59 以下で Geolocation を使った実装をしていた場合は、v0.60 以降 react-native-geolocation に移行作業をする必要があります。

*v0.59未満での記述*

```js
navigator.geolocation.setRNConfiguration(config);
```


*v0.60 以降での記述*

```js
import Geolocation from '@react-native-community/geolocation';

Geolocation.setRNConfiguration(config);
```

*または root ファイル内で、以下のように修正*

```js
navigator.geolocation = require('@react-native-community/geolocation');
```

#### Android で Geolocation を使用する場合の注意点

React Native の Geolocation を Android アプリで使用する場合も注意が必要です。
Geolocation は [android.location](https://developer.android.com/reference/android/location/package-summary) を使用しますが、この API は Google から正確性やパフォーマンスの観点から非推奨とされており、 [Google Location Services API](https://developer.android.com/training/location/) を使用することが推奨されています。

<img width="1093" alt="スクリーンショット 2019-12-20 7.59.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139589/b200da10-b01e-b019-57f9-788f0d336b98.png">

React Native で Google Location Services API を使用する場合は以下のサードパーティを使用するのが良さそうです。

-  [react-native-geolocation-service](https://github.com/Agontuk/react-native-geolocation-service) 
- [react-native-location](https://github.com/timfpark/react-native-location#readme)


# サンプルアプリを作ってみる
## 前提
では実際に react-native-geolocation を使って位置情報を取得し、地図表示するサンプルアプリを作ってみます。
今回作るサンプルアプリの `package.json` 抜粋です。

```package.json
  "dependencies": {
    "react": "16.9.0",
    "react-native": "0.61.2",
    "@react-native-community/geolocation": "2.0.2",
    "react-native-maps": "0.26.1"
  },
```

地図表示には、React Native で地図を利用する場合によく使われる [react-native-maps](https://github.com/react-native-community/react-native-maps) を使って実装したいと思います。
また、今回は位置情報に関する実装にフォーカスした内容のため、後述するコード内で使用している以下については、あまり触れていません🙏

- react-native-maps
- Hooks
- TypeScript

## 導入

今回は React Native v0.61 でサンプルアプリを作るので、react-native-geolocation をインストールします。

```
$ npm install @react-native-community/geolocation --save
```

### 設定

#### iOS
アプリの使用時に位置情報を有効にするには、`Info.plist` に`NSLocationWhenInUseUsageDescription` と `NSLocationAlwaysAndWhenInUseUsageDescription` キーを含める必要があります。

また、バックグラウンド状態で Geolocation を有効にするには、`Info.plist` に`NSLocationAlwaysUsageDescription` キーを含め、Xcodeの 'Capabilities' タブからバックグラウンドモードとして `Location` を追加する必要があります。

<img width="780" alt="スクリーンショット 2019-12-22 17.14.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139589/aad07cb0-29f2-fd8f-15d5-ed084459da36.png">


### Android

Android で位置情報へのアクセスをリクエストするには、`AndroidManifest.xml` に次の行を追加する必要があります。

```xml
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
```

Android API >= 23 の場合、インストール時に許可されるものとは別にアプリ内でユーザーに許可を求める必要あります。[ドキュメント](https://github.com/react-native-community/react-native-geolocation#android)によると、対応していない場合にアプリがクラッシュする可能性があるとのことなので [PermissionsAndroid](https://facebook.github.io/react-native/docs/permissionsandroid) または [react-native-permissions](https://github.com/react-native-community/react-native-permissions) を使用して対応しましょう。

> Android API >= 23 Requires an additional step to check for, and request the ACCESS_FINE_LOCATION permission using the PermissionsAndroid API. Failure to do so may result in a hard crash.


## 現在位置情報を取得する

早速、現在位置情報を取得してみましょう。
react-native-geolocation でユーザーの現在地を取得する場合は、`getCurrentPosition()` を使います。

以下のコードで一旦位置情報は取得できます。
`compoonentDidMount()` などで位置情報を取得し、`setState()` するなどの使い方が一般的かなと思います。

*使用コード例*

```js
import Geolocation from '@react-native-community/geolocation';

...
  compoonentDidMount() {
    Geolocation.getCurrentPosition(
      position => this.setState({ position }),
      err => alert(err.message),
      { enableHighAccuracy: true, timeout: 10000, maximumAge: 10000 },
    );
  }
...
```

**コード解説**

`getCurrentPosition()` の第一引数には、成功時のコールバック関数を渡します。
コールバックの引数には、以下のようなオブジェクトが渡されます。



```json
{
  "timestamp": 132329930456.35,
  "coords": {
    "accuracy": 5,
    "altitude": 0,
    "altitudeAccuracy": -1,
    "heading": 217,
    "latitude": 35.652832,
    "longitude": 139.839478,
    "speed": 5.77
  }
}
```

位置情報は `coords` プロパティから参照でき、以下の情報にアクセスが可能です。

| プロパティ名 | 概要 |
| :-- | :-- |
| latitude | 緯度 |
| longitude | 経度 |
| altitude | 高度（m） |
| accuracy | 緯度、経度の精度 |
| altitudeAccuracy | 高度の精度 |
| heading | 方角（度） |
| speed | 速度（ｍ/S） |

その他、第二引数にはエラー時のコールバック関数を渡し、第三引数にはオプションを指定できます。

*指定できるオプションのキー*

- `timeout` (ms): 位置情報取得にかける最大時間。
- `maximumAge` (ms): 取得したキャッシュ値の最大キャッシュ時間。
- `enableHighAccuracy` (bool): GPS などにアクセスし、より正確な位置情報を取得する。

### 現在位置を地図で表示する

取得した位置情報から地図にピンを立ててみましょう。

<img width="411" alt="スクリーンショット 2019-12-22 16.07.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139589/3f79f762-cfd1-0bd0-223f-892fa119ba59.png">

以下がソースコードです。

```tsx
import React, { useState, useEffect } from 'react';
import { SafeAreaView, View, Text, Dimensions, StyleSheet } from 'react-native';

import Geolocation, {
  GeolocationResponse,
} from '@react-native-community/geolocation';
import MapView, { Marker } from 'react-native-maps';

const { width, height } = Dimensions.get('window');
const ASPECT_RATIO = width / height;
const LATITUDE_DELTA = 0.01;
const LONGITUDE_DELTA = LATITUDE_DELTA * ASPECT_RATIO;

const CurrentUserMapView = () => {
  const [position, setPosition] = useState<GeolocationResponse['coords']>({
    latitude: 0,
    longitude: 0,
    accuracy: 0,
    altitude: 0,
    altitudeAccuracy: 0,
    heading: 0,
    speed: 0,
  });

  useEffect(() => {
    Geolocation.getCurrentPosition(
      position => setPosition(position.coords),
      err => alert(err.message),
      { enableHighAccuracy: true, timeout: 10000 },
    );
  }, []);

  return (
    <SafeAreaView style={styles.container}>
      {position && (
        <MapView
          style={styles.map}
          initialRegion={{
            latitude: position.latitude,
            longitude: position.longitude,
            latitudeDelta: LATITUDE_DELTA,
            longitudeDelta: LONGITUDE_DELTA,
          }}>
          <Marker
            coordinate={{
              latitude: position.latitude,
              longitude: position.longitude,
            }}
          />
          {/* Debug 用に coords オブジェクトを表示 */}
          <View style={styles.debugContainer}>
            <Text>{`coords: {`}</Text>
            {Object.keys(position).map(key => {
              return <Text key={key}>{`  ${key} : ${position[key]}`}</Text>;
            })}
            <Text>{`}`}</Text>
          </View>
        </MapView>
      )}
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    width: '100%',
    height: '100%',
  },
  map: {
    flex: 1,
  },
  debugContainer: {
    backgroundColor: '#fff',
    opacity: 0.8,
  },
});

export default CurrentUserMapView;
```

**コード解説**

`getCurrentPosition()` で取得した緯度/経度情報 を react-native-maps の　`MapView`, `Maker` コンポーネントに渡すだけで、地図に表示することができました。
今回は iOS のデフォルト地図アプリで表示していますが、[Google Map で表示することも可能](https://github.com/react-native-community/react-native-maps/blob/master/docs/installation.md#enabling-google-maps-on-ios-react-native-all-versions)です。 


## 追跡した位置情報を取得する

続いて、`watchPosition()` を使用することで、ユーザーを追跡して位置情報を取得することができます。

*使用コード例*

```js
import Geolocation from '@react-native-community/geolocation';

...
  componentDidMount() {
    this._watchID = Geolocation.watchPosition(
      position => this.setState({ position });
      error => alert(error.message),
      { enableHighAccuracy: true, timeout: 10000, distanceFilter: 10 },
    );
  }

  componentWillUnmount() {
    Geolocation.clearWatch(this._watchID);
  }
...
```

**コード解説**

`watchPosition()` は `getCurrentPosition()` とインターフェイスとしては同じように扱えますが、以下のような違いがあります。

- オプションとして以下を指定できる。
  - `distanceFilter` (m): 新たな位置情報を返すユーザーの最短移動距離。
  - `useSignificantChanges` (bool): バッテリー効率の高いネイティブの API を使用する。
- 返り値である `watchId` を取得して、アンマウント時などに `clearWatch()` or `stopObserving()` する。


### 追跡した位置情報を地図で表示する

現在位置を地図で表示した時と同じように、ユーザーの追跡を地図で表示してみましょう。


![Dec-22-2019 11-10-11.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139589/cfc68385-429d-018b-7c16-97df0e4ef5ff.gif)


以下がソースコードです。

```tsx
import React, { useState, useEffect } from 'react';
import { SafeAreaView, View, Text, Dimensions, StyleSheet } from 'react-native';

import Geolocation, {
  GeolocationResponse,
} from '@react-native-community/geolocation';

import MapView, { Marker } from 'react-native-maps';

const { width, height } = Dimensions.get('window');
const ASPECT_RATIO = width / height;
const LATITUDE_DELTA = 0.01;
const LONGITUDE_DELTA = LATITUDE_DELTA * ASPECT_RATIO;

const TrackUserMapView = () => {
  const [position, setPosition] = useState<GeolocationResponse['coords']>({
    latitude: 0,
    longitude: 0,
    accuracy: 0,
    altitude: 0,
    altitudeAccuracy: 0,
    heading: 0,
    speed: 0,
  });

  useEffect(() => {
    const watchId = Geolocation.watchPosition(
      position => setPosition(position.coords),
      err => alert(err.message),
      { enableHighAccuracy: true, timeout: 10000, distanceFilter: 1 },
    );
    return () => Geolocation.clearWatch(watchId);
  }, []);

  return (
    <SafeAreaView style={styles.container}>
      {position && (
        <MapView
          style={styles.map}
          initialRegion={{
            latitude: position.latitude,
            longitude: position.longitude,
            latitudeDelta: LATITUDE_DELTA,
            longitudeDelta: LONGITUDE_DELTA,
          }}>
          <Marker
            image={require('./walking_man.png')}
            coordinate={{
              latitude: position.latitude,
              longitude: position.longitude,
            }}
          />
          {/* Debug 用に coords オブジェクトを表示 */}
          <View style={styles.debugContainer}>
            <Text>{`coords: {`}</Text>
            {Object.keys(position).map(key => {
              return <Text key={key}>{`  ${key} : ${position[key]}`}</Text>;
            })}
            <Text>{`}`}</Text>
          </View>
        </MapView>
      )}
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    width: '100%',
    height: '100%',
  },
  map: {
    flex: 1,
  },
  debugContainer: {
    backgroundColor: '#fff',
    opacity: 0.8,
  },
});

export default TrackUserMapView;

```


**コード解説**

`useEffect()` 内の処理以外は、現在位置を地図で表示した場合と基本的に同じコードです。
今回は人間が走る速度でも、ある程度の動きが欲しかったので、オプション `distanceFilter` には `1` を指定しています。

また、ピンが動くのも味気なかったので、オープンソースである [Twemoji](https://github.com/twitter/twemoji) を使って、マーカーを表示しました。
`<Marker />` の `props.image` に画像リソースを渡すだけでピンを好きなものに変更できます。

#### Simulator でのデバック
Simulator では、位置情報のデバック用に設定があり、メニューの [Debug] > [Location] から、シーンや緯度/経度の指定ができます。
上記のキャプチャでは City Run でデバックしています。

| 項目 | 概要 |
| ---- | ---- |
| None | 位置指定なし |
| Custom Location | 緯度/経度を指定 |
| City Run | 走る速度で移動 |
| City Bicycle Ride | 自転車の速度で移動 |
| Freeway Drive | 車の速度で移動 |
| Apple | Apple 本社の座標 (default) |

実際に他のモードも試してみましたが、角を曲がるときに速度が落ちたり、信号待ちで結構な時間動かなかったり (最初バグかと思う) 忠実に現実の動きを再現しているようです。
どのモードでも Apple 本社の周りからどこかに向かって移動しています。

![Dec-22-2019 11-15-05.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139589/2ff248fb-db53-93c1-f804-ba772b5d1af2.gif)![Dec-22-2019 11-12-26.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139589/8399c83a-0754-8622-46e3-f7832d34b768.gif)

ちなみに上記 Freeway Drive の場合では、マーカーがすぐに地図外へ移動してしまうので、常に地図の中心に来るように `MapView` の `region` を動的に変更しています。

```diff:jsx
        <MapView
          style={styles.map}
-          initialRegion={{
+          region={{
            latitude: position.latitude,
            longitude: position.longitude,
            latitudeDelta: LATITUDE_DELTA,
            longitudeDelta: LONGITUDE_DELTA,
          }}>
```

# おわりに
以上、簡単な React Native サンプルアプリで位置情報を地図表示する方法の紹介でした。

実際にユーザーに使ってもらうアプリとなると、位置情報の取得処理がユーザーデバイスの環境に依存してしまうことや、パーミッション周りの体験設計、iOS/Android それぞれの規約を順守することなど、大変なことは出てきそうです。
位置情報は個人情報としても重要なものと一般的に解釈されており、先日リリースされた iOS13 でも位置情報周りのセキュリティが強化され、パーミッション設定にもアップデートもありました。Android でも今年に Android Q でパーミッション設定に似たようなアップデートがあったばかりなので、この辺りはこれからも変化が続きそうです。

参考:

- [Google Developers Japan: ユーザーが位置情報データをさらに細かく制御できるようになります](https://developers-jp.googleblog.com/2019/04/giving-users-more-control-over-their.html)
- [What's New in Core Location -WWDC 2019](https://developer.apple.com/videos/play/wwdc2019/705/)
- [Designing for Privacy - WWDC 2019](https://developer.apple.com/videos/play/wwdc2019/708)

セキュリティ、バックグラウンド時のバッテリー消費の観点など、アプリの位置情報取得についてはまだまだ課題があるようです。個人的にこの先の流れにも興味があるので、今後も追っていく中で何か情報発信ができればと思っています。

最後まで読んでいただきありがとうございました。
本記事の内容で誤り or ここはこうできるよなどあれば、コメントいただけますと大変嬉しいです！

明日の [React Native Advent Calendar 2019](https://qiita.com/advent-calendar/2019/react-native) は 「[PIXTA](https://pixta.jp/?waad=YjPYgMhl&gclid=EAIaIQobChMI7Nml9q3J5gIVCWoqCh1mxQBIEAAYASAAEgKYovD_BwE)」の @naoshihoshi さんによる記事です。よろしくお願いします！