Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

この記事はくふうカンパニー Advent Calendar 2018の16日目の記事です。

はじめに

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

以前に趣味で Swift による iOSアプリを個人制作したことがあるのですが、近頃また個人的な趣味でモバイルアプリを作りたくなり、せっかくなので React の知識が活かせそうな React Native を試してみよう! ということで、今回 React Native でiOSアプリを作ってみました。

散歩が好きなので、オープンソースの地図である OpenStreetMap を使って、散歩に使えるような簡単な地図アプリを作っていきます。

TL;DR

こんなふうになりました。リポジトリはこちら
Simulator Screen Shot - iPhone X - 2018-12-15 at 15.33.03 copy 2.jpg

React Native

React Native はモバイルアプリ用のフレームワークで、 React で Web のフロントエンドを実装するのと同じ感覚で iOS と Android 両方のアプリを作れるのが大きな特徴です。

React Native のインストール

公式ドキュメントにしたがってインストールしていきます。
今回の開発環境は Mac なので、 Homebrew を使っています。
(node あたりはインストール済の方も多いと思いますが、念のため)

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

手元の環境ではこの次の react-native コマンド呼び出しがうまくいかなかったので、こちらの記事を参考にして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シミュレーターにアプリの初期画面が表示されます!

Simulator Screen Shot - iPhone X - 2018-12-15 at 11.54.49.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 の場合とは異なります。 pxpt 等の単位は使わず、絶対値で指定するようです。(こちらの記事が参考になりました)

Simulator Screen Shot - iPhone X - 2018-12-15 at 12.25.57.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 という地図コンポーネントが用意されており、これを追加するだけで地図を表示することができました。

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

こちらも公式ガイドに沿って進めていきます。
Google Maps を使う場合は CocoaPods による依存関係の追加が必要ですが、今回は使わないので、コマンドを実行するだけで react-nativereact-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.stateregion に持たせます。

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標準の地図が表示されます!
Simulator Screen Shot - iPhone X - 2018-12-15 at 14.28.15.png

地図を OpenStreetMaps に差し替える

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

UrlTile コンポーネントを追加

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

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

App.js
import MapView, { UrlTile, Marker } from 'react-native-maps';
    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: '田町ニューデイズ',
        },
      ],
    };

  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

デバイスから現在地を検出して地図の中心にする

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

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

今回は実機でなくiOSシミュレーターで動作検証したので、こちらの記事を参考にシミュレーター側で現在地(高輪ゲートウェイ駅)を設定しました。
メニューの Debug -> Location -> Custom Location... と選んでいくと、下図のウィンドウで現在地の緯度経度を入力できます。

Screen Shot 2018-12-15 at 16.24.34.png

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

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

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

    this.state = {
      // 中略
    };

    this.requestToGetCurrentPosition();
  }
  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

完成

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

おわりに

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

最終型の App.js 実装はこちら

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした