この記事はくふうカンパニー Advent Calendar 2018の16日目の記事です。
はじめに
こんにちは。オウチーノ技術部の濱崎です。
ふだんは Rails や React (ときどき Scala) でWebアプリのフロントエンド〜サーバーサイドを実装しています。
以前に趣味で Swift による iOSアプリを個人制作したことがあるのですが、近頃また個人的な趣味でモバイルアプリを作りたくなり、せっかくなので React の知識が活かせそうな React Native を試してみよう! ということで、今回 React Native でiOSアプリを作ってみました。
散歩が好きなので、オープンソースの地図である OpenStreetMap を使って、散歩に使えるような簡単な地図アプリを作っていきます。
TL;DR
こんなふうになりました。リポジトリはこちら。
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の追加も行いました。
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シミュレーターにアプリの初期画面が表示されます!
React Native 側の準備はこれで完了です。
スタイルを編集する
React Native の大きな利点として、モバイルアプリの開発でありながら React による Web アプリの開発とほとんど同じ感覚で進められることがあります。
デフォルトで生成された App.js
を見てみても、内容は React のJSXコードとほとんど変わらないように見えますね。
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
等の単位は使わず、絶対値で指定するようです。(こちらの記事が参考になりました)
書き換えたコードは以下のとおりです。
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-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
に持たせます。
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標準の地図が表示されます!
地図を OpenStreetMaps に差し替える
このままでも地図としては使えますが、今回は OpenStreetMap を使ってみたかったので、iOS標準地図からの差し替えを行います。
UrlTile コンポーネントを追加
とはいえ、 OpenStreetMap への差し替え方法もすでに react-native-maps 側で用意されていました、、!
UrlTile
コンポーネントを使うと、地図表面のタイルを urlTemplate
で指定した任意の地図に貼り替えることができます。 OpenStreetMap はこの貼り替え用のタイルを提供しているので、これまた公式ガイドのとおりに UrlTile
と urlTemplate
を追加するだけで貼り替え完了できてしまいます。
せっかくなので、地図中にピンを立てる Marker
コンポーネントも合わせて追加しました。
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 に差し替わり、ピンも立ちました。
(タイル内のテキストが小さくて読みづらいですね。このあたりは改善の余地がありそうです)
デバイスから現在地を検出して地図の中心にする
さて、これだけでも地図としては使えそうですが、せっかくのモバイルアプリなので、地図を開いたときには現在地が反映されてほしいですね。
というわけで今回は最後に、デバイスから現在地を検出して、アプリ起動時に地図の中心が現在地になるようにします。
iOSシミュレーターで現在地を設定
今回は実機でなくiOSシミュレーターで動作検証したので、こちらの記事を参考にシミュレーター側で現在地(高輪ゲートウェイ駅)を設定しました。
メニューの Debug
-> Location
-> Custom Location...
と選んでいくと、下図のウィンドウで現在地の緯度経度を入力できます。
JS側に緯度経度検出機能を追加
次に、この緯度経度を検出する方法ですが、これは React Native のライブラリに頼るまでもなく、 JavaScript 側で navigator.geolocation.getCurrentPosition()
というメソッドが用意されています。
今回はこれを利用して、 constructor()
内で現在地の緯度経度が state.region
にセットされるようにしました。
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}`);
}
実行結果がこちら。ちゃんと高輪ゲートウェイが真ん中になっています。
完成
高輪ゲートウェイを散歩していて迷子になってもぶじ田町に辿り着けるようになりました。めでたし。
おわりに
本当にふだん React を書いているのとほとんど同じ感覚でモバイルアプリが作れてしまいました。
今回はピンの情報がひとつだけでしたが、例えばバックエンド側で駅の緯度経度情報と組み合わせて最寄りの駅をサジェストしたり、いろいろな機能が 妄想 展開できそうです。
最終型の App.js
実装はこちら。