viviONグループでは、DLsiteやcomipoなど、二次元コンテンツを世の中に届けるためのサービスを運営しています。
ともに働く仲間を募集していますので、興味のある方はこちらまで。
普段はVue3(Nuxt3)をメインでWeb開発をしています。
今回はReact NativeをベースとしたExpoというフレームワークを試してみます。
(Web / Androidでの動作を確認します)
プロジェクトの作成
今回はクリスマスイベントスポット(東京)の表示を試してみます(申し訳程度のクリスマス要素)
イベント情報はただ力技で調べました 🙈
npx create-expo-app@latest example-expo-chiristmas-app
npx expo start
これだけでAndroid + Webの開発が可能な状態になりました
確認できたところでひとまずnpm run reset-project
をしておきます
📱 Android 実機で確認する
Expo Goというアプリを利用してQRコードを読み込むだけで開発中の環境にアクセスできます
https://expo.dev/go
Expo Goは、Expo専用のサンドボックス環境で、よく使用されるライブラリは予めプリインストールされているので、開発を素早く始めることができます
このページでExpo Goの ✅ が入っているものがプリインストールされているようです。
https://reactnative.directory/
暫くの開発はこれで十分そうです!
ここまで確認出来たらnpm run reset-project
を実行します。
(最初のapp
ディレクトリをapp-example
に移動することができます)
マップを表示する
webとネイティブアプリで別の方法を用いて実装します。
Expo Routerの機能でwebとネイティブアプリ用に実装を分けることができます。
components
├── map.native.tsx
└── map.web.tsx
.native.tsx
や.web.tsx
のような拡張子をつけることでプラットフォームを元に出し分けが可能です。
appディレクトリでは、この拡張子はされてないため、別途componentsディレクトリを作成します。
実際切り分けて表示するにはレイアウト側でのエクスポートが必要です。
export { default } from './components/map';
Webの表示
お手軽にGoogleマイマップを利用して、iframe
で埋め込みます。
マイマップでイベントスポットのピンを挿したら、「自分のサイトに埋め込む」からiframeタグを取得します。
そのままiframe
を設置したのみでは、初期位置が好ましくありませんでした。
初期位置の指定として、navigator.geolocation
を利用して、現在地を中心にします。
(navigator.geolocation
を呼び出すとブラウザから位置情報の取得権限がリクエストされるはずです)
位置情報の取得が許可されなかった場合は、皇居を中央にするとします。
中心の緯度/経度をll
パラメータで指定できるので、セットします。
また、縮尺についてはz
パラメータで指定できるようなので、併せて微調整します。
import { useState, useEffect } from "react";
// 皇居の緯度、経度
const INITIAL_LATITUADE = 35.68546261882921
const INITIAL_LONGITUDE = 139.75286760095705
export default function Map() {
const [location, setLocation] = useState<{latituade: number, longitude: number} | null>(null);
const [iframeUrl, setIframeUrl] = useState(`https://www.google.com/maps/d/u/0/embed?mid=1BxutSC-ayz-IgADpmBl7WJfnqHsJiTU&ehbc=2E312F&noprof=1&z=14&ll=${INITIAL_LATITUADE},${INITIAL_LONGITUDE}`);
useEffect(() => {
if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition((position) => {
// 現在地をセットする
setLocation({ latituade: position.coords.latitude, longitude: position.coords.longitude })
})
}
}, [])
useEffect(() => {
const url = new URL(iframeUrl);
const searchParams = url.searchParams;
if (location) {
// 緯度、経度をパラメータに再設定する
searchParams.set('ll', `${location.latituade},${location.longitude}`);
}
setIframeUrl(url.toString())
}, [location])
return (
<div style={styles.container}>
<iframe style={styles.map} src={iframeUrl}></iframe>
</div>
);
}
const styles = {
container: {
width: '100%',
height: '100%',
backgroundColor: '#fff',
},
map: {
width: '100%',
height: 'calc(100% + 46px)',
marginTop: -46, // 見栄えのためにiframeにヘッダーを隠す
},
};
📱 ネイティブアプリの表示
マップの表示には、react-native-mapview
を利用しました。
基本的には<MapView>
コンポーネント内で地図を表示、<Marker>
コンポーネントに緯度/経度をセットすることで、イベントスポットの表示ができました。
(ちなみにウェブで作成したマイマップをCSVでexportして緯度/経度を取ってきました)
初期位置の指定も同様に行いたいので、expo-location
を用いることにします。
位置情報の取得権限をリクエストして、<MapView>
コンポーネントのregion
で利用する形にしています。
import { useState, useEffect } from 'react';
import { View, StyleSheet } from "react-native";
import MapView, { Marker } from 'react-native-maps';
import * as Location from 'expo-location';
const INITIAL_LATITUADE = 35.68546261882921;
const INITIAL_LONGITUDE = 139.75286760095705;
const LATITUADE_DELTA = 0.04;
const LONGITUDE_DELTA = 0.04;
const POSITIONS = [
{longitude: 139.7292319, latitude: 35.6607397, name: '六本木ヒルズ'},
{longitude: 139.7308747, latitude: 35.6659803, name: '東京ミッドタウン'},
{longitude: 139.7182668, latitude: 35.67543990000001, name: '明治神宮外苑'},
{longitude: 139.7638027, latitude: 35.6810403, name: '丸の内ビルディング'},
{longitude: 139.8115747, latitude: 35.7102333, name: '東京ソラマチ'},
{longitude: 139.7734312, latitude: 35.7147557, name: '上野恩賜公園'},
// ※省略します
]
export default function Map() {
const [location, setLocation] = useState<Location.LocationObject | null>(null);
useEffect(() => {
async function getCurrentLocation() {
let { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
return;
}
let location = await Location.getCurrentPositionAsync({});
setLocation(location);
}
getCurrentLocation();
}, []);
return (
<View style={styles.container}>
<MapView
style={styles.map}
provider={PROVIDER_GOOGLE}
initialRegion={{
latitude: INITIAL_LATITUADE,
longitude: INITIAL_LONGITUDE,
latitudeDelta: LATITUADE_DELTA,
longitudeDelta: LONGITUDE_DELTA,
}}
region={{
latitude: location?.coords.latitude ?? INITIAL_LATITUADE,
longitude: location?.coords.longitude ?? INITIAL_LONGITUDE,
latitudeDelta: LATITUADE_DELTA,
longitudeDelta: LONGITUDE_DELTA,
}}
>
{POSITIONS.map((pos, index) =>
<Marker
key={index}
coordinate={{
latitude: pos.latitude,
longitude: pos.longitude,
}}
title={pos.name}
/>
)}
</MapView>
</View>
);
}
const styles = StyleSheet.create({
container: {
width: '100%',
height: '100%',
backgroundColor: '#fff',
},
map: {
width: '100%',
height: '100%',
},
});
web | アプリ(Android) |
---|---|
💭 感想
- なんとなくネイティブアプリは実装も検証も大変そう...な印象でしたが、Expo Goでの開発体験は革命的でした
- Androidアプリの開発ビルドも試しましたが、EAS Buildでお手軽でした(ただし無料枠だと月30回までのようです)
参考
https://docs.expo.dev/router/advanced/platform-specific-modules/
https://docs.expo.dev/versions/latest/sdk/map-view/
https://docs.expo.dev/versions/latest/sdk/location/
一緒に二次元業界を盛り上げていきませんか?
株式会社viviONでは、フロントエンドエンジニアを募集しています。
また、フロントエンドエンジニアに限らず、バックエンド・SRE・スマホアプリなど様々なエンジニア職を募集していますので、ぜひ採用情報をご覧ください。