React Native と OpenStreetMap で散歩用のiOS地図アプリを作ってみた

この記事は[くふうカンパニー Advent Calendar 2018](https://qiita.com/advent-calendar/2018/kufu)の16日目の記事です。

# はじめに
こんにちは。オウチーノ技術部の濱崎です。
ふだんは Rails や React (ときどき Scala) でWebアプリのフロントエンド〜サーバーサイドを実装しています。

以前に趣味で Swift による [iOSアプリを個人制作した](https://itunes.apple.com/bj/app/id1170863272)ことがあるのですが、近頃また個人的な趣味でモバイルアプリを作りたくなり、せっかくなので React の知識が活かせそうな React Native を試してみよう！ ということで、今回 React Native でiOSアプリを作ってみました。

散歩が好きなので、オープンソースの地図である [OpenStreetMap](https://www.openstreetmap.org/#map=5/35.588/134.380) を使って、散歩に使えるような簡単な地図アプリを作っていきます。

# TL;DR
こんなふうになりました。リポジトリは[こちら](https://github.com/lfcd85/react-native-openstreetmap-app)。
![Simulator Screen Shot - iPhone X - 2018-12-15 at 15.33.03 copy 2.jpg](https://qiita-image-store.s3.amazonaws.com/0/213209/8241859c-0404-48b9-78ab-b4ce1d000ad1.jpeg)

# React Native
[React Native](https://facebook.github.io/react-native/) はモバイルアプリ用のフレームワークで、 [React](https://reactjs.org/) で Web のフロントエンドを実装するのと同じ感覚で iOS と Android 両方のアプリを作れるのが大きな特徴です。

## React Native のインストール
[公式ドキュメント](https://facebook.github.io/react-native/docs/getting-started)にしたがってインストールしていきます。
今回の開発環境は Mac なので、 Homebrew を使っています。
（node あたりはインストール済の方も多いと思いますが、念のため）

```
brew install node
brew install watchman
npm install -g react-native-cli
```

手元の環境ではこの次の `react-native` コマンド呼び出しがうまくいかなかったので、[こちらの記事](https://qiita.com/umechiki/items/a1de903a2e5e27f5c606)を参考にしてPATHの追加も行いました。

```~/.bash_profile
if [ -d ${HOME}/node_modules/.bin ]; then
    export PATH=${PATH}:${HOME}/node_modules/.bin
fi
```

```
source ~/.bash_profile
```

パスが通ったら、アプリの初期化とiOS用のビルドをするコマンドを流します。

```
react-native init react-native-openstreetmap-app
cd react-native-openstreetmap-app
react-native run-ios
```

しばらくするとビルドが完了し、iOSシミュレーターにアプリの初期画面が表示されます！

<img width="375" alt="Simulator Screen Shot - iPhone X - 2018-12-15 at 11.54.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/213209/73951e7a-e42e-031b-23fb-8a474af580f5.png">

React Native 側の準備はこれで完了です。

## スタイルを編集する

React Native の大きな利点として、モバイルアプリの開発でありながら React による Web アプリの開発とほとんど同じ感覚で進められることがあります。
デフォルトで生成された `App.js` を見てみても、内容は React のJSXコードとほとんど変わらないように見えますね。

```App.js
import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View} from 'react-native';

const instructions = Platform.select({
  ios: 'Press Cmd+R to reload,\n' + 'Cmd+D or shake for dev menu',
  android:
    'Double tap R on your keyboard to reload,\n' +
    'Shake or press menu button for dev menu',
});

type Props = {};
export default class App extends Component<Props> {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>Welcome to React Native!</Text>
        <Text style={styles.instructions}>To get started, edit App.js</Text>
        <Text style={styles.instructions}>{instructions}</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
});
```

せっかくなので、この初期画面も少し編集してみました。
レイアウトも CSS in JS の形式で記述できるようになっていますが、フォントの大きさやマージン等の単位は Web の場合とは異なります。 `px` や `pt` 等の単位は使わず、絶対値で指定するようです。（[こちらの記事](https://qiita.com/Omoti/items/b8a3f1843f36d7374462)が参考になりました）

<img width="375" alt="Simulator Screen Shot - iPhone X - 2018-12-15 at 12.25.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/213209/a4717aec-0072-b851-e8ab-36edbadfee5f.png">

書き換えたコードは以下のとおりです。

```App.js
const instructions = Platform.select({
  ios:
    'iOS と Android で表示の出し分けができそうです。\n' +
    'Cmd+R キーでリロードしたり、 Cmd+D キーでメニューを出したりできるとのこと。',
  android:
    'ここは Android 用なので、編集しても iOS では表示されなさそう。\n' +
    'せっかくなのでスタイルシートもちょっと変えてみます。',
});

type Props = {};
export default class App extends Component<Props> {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>ここに地図を出したい</Text>
        <Text style={styles.instructions}>OpenStreetMapを使ってみたい</Text>
        <Text style={styles.instructions}>{instructions}</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#ffffff',
  },
  welcome: {
    fontSize: 24,
    fontWeight: '600',
    color: '#333333',
    textAlign: 'center',
    marginBottom: 40,
  },
  instructions: {
    fontSize: 14,
    lineHeight: 20,
    textAlign: 'left',
    color: '#666666',
    marginHorizontal: 30,
    marginBottom: 30,
  },
});
```

# OpenStreetMap の地図を追加

次はいよいよ、アプリに地図を追加していきます。
幸い React Native コミュニティによって [react-native-maps](https://github.com/react-native-community/react-native-maps) という地図コンポーネントが用意されており、これを追加するだけで地図を表示することができました。

## react-native-maps をインストール

こちらも[公式ガイド](https://github.com/react-native-community/react-native-maps/blob/master/docs/installation.md)に沿って進めていきます。
Google Maps を使う場合は CocoaPods による依存関係の追加が必要ですが、今回は使わないので、コマンドを実行するだけで `react-native` と `react-native-maps` を接続できました。
（ただし、手元の環境では2行目の `npm install` を行わないと3行目でエラーが出ました）

```
npm install react-native-maps --save
npm install
react-native link react-native-maps
```

## 画面を地図コンポーネントに差し替える

`react-native-map` がインストールできたら、いよいよ `App.js` の実装を地図コンポーネント用に書き換えます。
公式ドキュメントと同様に `MapView` を import して `render()` で呼び出し、緯度経度情報は `this.state` の `region` に持たせます。

```App.js
import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View} from 'react-native';
import MapView from 'react-native-maps';

type Props = {};
export default class App extends Component<Props> {
  constructor(props) {
    super(props);
    this.state = {
      region: {
        latitude: 35.645736,
        longitude: 139.747575,
        latitudeDelta: 0.03,
        longitudeDelta: 0.03,
      },
    };
  }

  onRegionChange(region) {
    this.setState({ region });
  }

  render() {
    return (
      <View style={styles.container}>
        <MapView
          style={styles.map}
          region={this.state.region}
          onRegionChange={this.onRegionChange.bind(this)}
        />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    ...StyleSheet.absoluteFillObject,
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#ffffff',
  },
  map: {
    ...StyleSheet.absoluteFillObject,
  },
});
```

ここで、コードを保存→リロードするだけではアプリに `react-native-maps` が反映されずエラーが出てしまったので、一旦下記のコマンドでビルドをやり直しました。
（このあたりはもっとよい方法があるかもしれません）

```
killall node
rm -rf ./ios/build/
react-native run-ios
```

ぶじにビルドできたら、下図のようにiOS標準の地図が表示されます！
<img width="375" alt="Simulator Screen Shot - iPhone X - 2018-12-15 at 14.28.15.png" src="https://qiita-image-store.s3.amazonaws.com/0/213209/b4f52fff-d437-0f73-6bf8-4e92b13fc19f.png">

# 地図を OpenStreetMaps に差し替える

このままでも地図としては使えますが、今回は OpenStreetMap を使ってみたかったので、iOS標準地図からの差し替えを行います。

## UrlTile コンポーネントを追加

とはいえ、 OpenStreetMap への差し替え方法もすでに react-native-maps 側で用意されていました、、！
`UrlTile` コンポーネントを使うと、地図表面のタイルを `urlTemplate` で指定した任意の地図に貼り替えることができます。 OpenStreetMap はこの貼り替え用のタイルを提供しているので、これまた公式ガイドのとおりに `UrlTile` と `urlTemplate` を追加するだけで貼り替え完了できてしまいます。

せっかくなので、地図中にピンを立てる `Marker` コンポーネントも合わせて追加しました。

```App.js
import MapView, { UrlTile, Marker } from 'react-native-maps';
```

```javascript
    this.state = {
      region: {
        // 中略
      },
      urlTemplate: 'http://c.tile.openstreetmap.org/{z}/{x}/{y}.png',
      markers: [
        {
          key: 'tamachiStation',
          latlng: {
            latitude: 35.645736,
            longitude: 139.747575,
          },
          title: '田町駅',
          description: '田町ニューデイズ',
        },
      ],
    };

```
```javascript
  render() {
    return (
      <View style={styles.container}>
        <MapView
          style={styles.map}
          region={this.state.region}
          onRegionChange={this.onRegionChange.bind(this)}
        >
          <UrlTile
            urlTemplate={this.state.urlTemplate}
            maximumZ={19}
          />
          {this.state.markers.map(marker => (
            <Marker
              key={marker.key}
              coordinate={marker.latlng}
              title={marker.title}
              description={marker.description}
            />
          ))}
        </MapView>
      </View>
    );
  }
```

結果がこちら。地図が OpenStreetMap に差し替わり、ピンも立ちました。
（タイル内のテキストが小さくて読みづらいですね。このあたりは改善の余地がありそうです）
![Simulator Screen Shot - iPhone X - 2018-12-15 at 15.33.03 copy 2.jpg](https://qiita-image-store.s3.amazonaws.com/0/213209/8241859c-0404-48b9-78ab-b4ce1d000ad1.jpeg)

# デバイスから現在地を検出して地図の中心にする
さて、これだけでも地図としては使えそうですが、せっかくのモバイルアプリなので、地図を開いたときには現在地が反映されてほしいですね。
というわけで今回は最後に、デバイスから現在地を検出して、アプリ起動時に地図の中心が現在地になるようにします。

## iOSシミュレーターで現在地を設定

今回は実機でなくiOSシミュレーターで動作検証したので、[こちらの記事](https://qiita.com/mattak/items/420039c674fe4392a647)を参考にシミュレーター側で現在地（[高輪ゲートウェイ駅](https://ja.wikipedia.org/wiki/%E9%AB%98%E8%BC%AA%E3%82%B2%E3%83%BC%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A4%E9%A7%85)）を設定しました。
メニューの `Debug` -> `Location` -> `Custom Location...` と選んでいくと、下図のウィンドウで現在地の緯度経度を入力できます。

<img width="392" alt="Screen Shot 2018-12-15 at 16.24.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/213209/9c2934fb-d326-092d-0642-0448033f254b.png">

## JS側に緯度経度検出機能を追加

次に、この緯度経度を検出する方法ですが、これは React Native のライブラリに頼るまでもなく、 JavaScript 側で `navigator.geolocation.getCurrentPosition()` というメソッドが[用意されています](https://developer.mozilla.org/ja/docs/Web/API/Geolocation/getCurrentPosition)。
今回はこれを利用して、 `constructor()` 内で現在地の緯度経度が `state.region` にセットされるようにしました。

```App.js
export default class App extends Component<Props> {
  constructor(props) {
    super(props);

    this.state = {
      // 中略
    };

    this.requestToGetCurrentPosition();
  }
```

```javascript
  requestToGetCurrentPosition = () => {
    if (!navigator.geolocation) return false

    const options = {
      enableHighAccuracy: true,
      timeout: 5000,
      maximumAge: 0,
    };

    navigator.geolocation.getCurrentPosition(
      this.successToGetCurrentPosition,
      this.failToGetCurrentPosition,
      options
    );
  }

  successToGetCurrentPosition = (position) => {
    const { latitude, longitude } = position.coords;

    this.setState(prevState => ({
      region: {
        ...prevState.region,
        latitude,
        longitude,
      }
    }));
  }

  failToGetCurrentPosition = (error) => {
    console.warn(`ERROR(${error.code}): ${error.message}`);
  }
```

実行結果がこちら。ちゃんと高輪ゲートウェイが真ん中になっています。
![Simulator Screen Shot - iPhone X - 2018-12-15 at 16.23.08 copy 2.jpg](https://qiita-image-store.s3.amazonaws.com/0/213209/993bb61e-50c1-986b-3179-9af34f94ce88.jpeg)

## 完成

高輪ゲートウェイを散歩していて迷子になってもぶじ田町に辿り着けるようになりました。めでたし。

# おわりに

本当にふだん React を書いているのとほとんど同じ感覚でモバイルアプリが作れてしまいました。
今回はピンの情報がひとつだけでしたが、例えばバックエンド側で駅の緯度経度情報と組み合わせて最寄りの駅をサジェストしたり、いろいろな機能が ~~妄想~~ 展開できそうです。

最終型の `App.js` 実装は[こちら](https://github.com/lfcd85/react-native-openstreetmap-app/blob/master/App.js)。