9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NRI OpenStandiaAdvent Calendar 2024

Day 23

【React Native+Expo】モバイルアプリ開発初心者がサクッと簡単に3つの機能を作ってみた

Posted at

はじめに

NRI OpenStandia Advent Calendar 2024 シリーズ2の 23日目担当のあやかです。

普段、私は業務でReactを使いWebアプリを開発していますが、モバイルアプリ開発にも興味がありました。ただ、なかなか最初の一歩を踏み出せずにいました。

そこで今回は、できるだけ手軽にモバイルアプリ開発を始める方法に挑戦してみようと思います!
React開発者にとっては学習コストが低いReact Nativeを使い、さらに簡単に環境構築や動作確認ができるExpoというフレームワークを活用します。

「モバイルアプリ開発に興味はあるけど最初のハードルが高そう…」と思っている方も、この記事を読めば簡単に始められるはずです!
興味のある方は、ぜひ読んでみてください。

環境作成

React Native 公式サイトを参考にして、開発環境を作成していきます。

React Nativeの開発には、フレームワークを使う方法と使わない方法がありますが、公式サイトではフレームワークを使う方法が推奨されています。また、その方が手軽に始められるため、今回はExpoというフレームワークを使って環境を構築しました。

手順

①Node.jsインストール
Node.jsがまだインストールされていない場合は、公式サイトからインストールしてください。
今回はv22.12.0を使用しました。(2024/12/20時点の推奨バージョン)

②プロジェクト作成
以下のコマンドを実行して、Expoアプリのプロジェクトを作成します。

npx create-expo-app@latest

③Expoのユーザーアカウントを作成
Expoの公式サイトからユーザーアカウントを作成してください。

④Expo Goアプリをスマホにインストール
開発アプリをスマホで動作確認するため、Expo Goアプリをインストールします。
• iPhoneの場合: App Storeからダウンロード
• Androidの場合: Google Play Storeからダウンロード

今回はiPhoneを使用しました。詳細な手順はこちらを参考にしてください。

以上で準備は完了です!

動作確認

作成したプロジェクトのディレクトリに移動し、以下のコマンドを実行してアプリを起動します。

npm run start

コマンドを実行すると、以下のようにターミナルにQRコードが表示されます。

CBD06264-0165-4D57-8A63-D6A795699D10.jpeg

このQRコードをiPhoneのカメラで読み取り、Expo Goアプリで開きます。
これで、スマホ上で開発中のアプリの動作を確認することができます。

Init.gif

PCとデバッグ用のスマホが同じネットワークに接続されている必要があります。

コマンドプロンプトでrキーを押すと、iPhoneのExpo Goアプリがリロードされて表示が更新されます。

Expoのライブラリを使用して機能を作成する

環境作成手順2で作成したプロジェクトを基に、機能の追加や変更を行います。
Expoのドキュメントには、提供されている機能やその使い方が詳しく記載されているので、それを参考にしながら進めていきます。

カメラで写真撮影し保存する

カメラで写真撮影し、その写真を保存する機能を作成してみます。

まず、使うライブラリをインストールします。

npm install expo-camera expo-media-library

続いて、app>(tabs)配下に配置されている_layout.tsxにカメラタブを作成します。
タブに使用するiconは、端末がApple製品の場合
https://developer.apple.com/sf-symbols/
から探し、IconSymbolnameに指定します。

<Tabs.Screen
  name="photoPage"
  options={{
    title: 'Photo',
    tabBarIcon: ({ color }) => <IconSymbol size={28} name="camera" color={color} />,
  }}
/>

次に、app>(tabs)配下にphotoPage.tsxファイルを作成します。
Expoのドキュメントに従って、カメラの権限確認はuseCameraPermissionsというhooks、カメラのcomponentはCameraViewを使います。

import { useCameraPermissions } from 'expo-camera';

const [status, requestPermission] = useCameraPermissions();
import { CameraView } from 'expo-camera';

<CameraView style={styles.camera}>
  {/* Viewやボタン */}
</CameraView>

また、画像の保存に使うExpo MediaLibraryには、メディアライブラリの権限確認を行うrequestPermissionsAsyncや、メディアライブラリにメディアアセットを追加するcreateAssetAsync、指定された名前でフォルダ作成するcreateAlbumAsyncが用意されています。

import { requestPermissionsAsync } from 'expo-media-library';

const { status } = await MediaLibrary.requestPermissionsAsync();
import { createAssetAsync } from 'expo-media-library';

const asset = await MediaLibrary.createAssetAsync(uri);
import { createAlbumAsync } from 'expo-media-library';

const asset = await MediaLibrary.createAssetAsync(uri);

これらを使用して作成した機能がこちらです↓

Photo.gif

ソースコードの全量はこちらです。

photoPage.tsx
import React, { useState, useRef } from 'react';
import { View, Button, Text, StyleSheet, Image, ScrollView, Alert } from 'react-native';
import { CameraView, useCameraPermissions } from 'expo-camera';
import {requestPermissionsAsync, createAssetAsync, createAlbumAsync} from 'expo-media-library';

const PhotoPage = () => {
  const [permission, requestPermission] = useCameraPermissions();
  const [photo, setPhoto] = useState<string | null>(null);
  const cameraRef = useRef<CameraView | null>(null);
  const [mediaLibraryPermission, setMediaLibraryPermission] = useState<boolean | null>(null);

  // カメラの権限付与
  if (!permission) {
    return <View style={styles.container} />
  }
  if (!permission.granted) {
    return (
      <View style={styles.notGrantedContainer}>
        <Button onPress={requestPermission} title="カメラの起動を許可" />
      </View>
    );
  }

  // メディアライブラリの権限確認
  if (mediaLibraryPermission === null) {
    (async () => {
      const { status } = await requestPermissionsAsync();
      setMediaLibraryPermission(status === 'granted');
    })();
  }

  // 写真の撮影と保存
  const takePicture = async () => {
    if (cameraRef.current) {
      const photo = await cameraRef.current.takePictureAsync();
      if (photo) {
        setPhoto(photo.uri);

        // メディアライブラリへ保存
        if (mediaLibraryPermission) {
          try {
            const asset = await createAssetAsync(photo.uri);
            await createAlbumAsync('サンプルアプリフォルダ', asset, false); // 新しいアルバムに保存
            Alert.alert('保存完了', '写真がギャラリーに保存されました!');
          } catch (error) {
            console.error('写真の保存に失敗しました: ', error);
            Alert.alert('エラー', '写真の保存に失敗しました');
          }
        } else {
          Alert.alert('権限エラー', 'メディアライブラリのアクセス権限がありません');
        }
      }
    }
  }

  return (
    <ScrollView style={styles.container}>
      {/* カメラ部分 */}
      <View style={styles.cameraContainer}>
        <CameraView style={styles.camera} ref={cameraRef}>
          <View style={styles.buttonContainer}>
            <Button title="写真を撮影" onPress={takePicture} />
          </View>
        </CameraView>
      </View>

      {/* 撮影写真表示部分 */}
      <View style={styles.imageContainer}>
        {photo && (
          <View style={styles.photoContainer}>
            <Text>撮影した写真:</Text>
            <Image source={{ uri: photo }} style={styles.image} />
          </View>
        )}
      </View>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  notGrantedContainer: {
    flex: 1,
    justifyContent: 'center', 
    alignItems: 'center',   
  },
  container: {
    flex: 1,
  },
  cameraContainer: {
    height: 600,
    justifyContent: 'flex-end',
  },
  camera: {
    width: '100%',
    height: '100%',
    justifyContent: 'flex-end',
  },
  buttonContainer: {
    backgroundColor: 'transparent',
    flexDirection: 'row',
    justifyContent: 'center',
    marginBottom: 20, // ボタンをカメラビューの下に配置
  },
  imageContainer: {
    justifyContent: 'center',
    alignItems: 'center',
    marginTop: 20,
    paddingBottom: 300,
  },
  photoContainer: {
    alignItems: 'center',
  },
  image: {
    width: 300,
    height: 300,
    borderRadius: 10,
    marginTop: 10,
  },
});

export default PhotoPage;

画面の向きを固定する

次はスマホの画面の向きを指定した向きで固定する機能を作成していきます。

カメラ機能と同様に使うライブラリをインストールし、

npm install expo-screen-orientation

_layout.tsxに新しいタブを作成します。

<Tabs.Screen
  name="screenOrientationPage"
  options={{
    title: 'Orientation',
    tabBarIcon: ({ color }) => <IconSymbol size={28} name="iphone.gen3.circle" color={color} />,
  }}
/>

そして、app>(tabs)配下にscreenOrientationPage.tsxファイルを作成します。
Expoのドキュメントに従い、指定された向きに画面を固定するlockAsynck、画面の方向を指定するOrientationLock、画面の向きを端末で指定されているデフォルトの向きに戻すunlockAsyncを使います。

import { lockAsync, OrientationLock, unlockAsync } from 'expo-screen-orientation';

lockAsync(OrientationLock.PORTRAIT_UP);
unlockAsync();

これらを使用して作成したものがこちらです↓

ScreenOrientation.gif

ソースコードの全量はこちら↓

screenOrientationPage.tsx
import React, { useEffect } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { lockAsync, OrientationLock, unlockAsync, } from 'expo-screen-orientation';

const ScreenOrientationPage = () => {
  useEffect(() => {
    // マウント時の画面は縦向き固定
    lockAsync(OrientationLock.PORTRAIT_UP);

    // アンマウント時はデバイスで設定している向きに戻す
    return () => {
      unlockAsync();
    };
  }, []);

  const lockLandscape = async () => {
    // 横向きにロック
    await lockAsync(OrientationLock.LANDSCAPE);
  };

  const lockPortrait = async () => {
    // 縦向きにロック
    await lockAsync(OrientationLock.PORTRAIT_UP);
  };

  const unlockOrientation = async () => {
    // デバイスで設定している向きにする
    await unlockAsync();
  };

  return (
    <View style={styles.container}>
      <Text style={styles.text}>画面の向きの制御サンプル</Text>
      <Button title="横向きにロック" onPress={lockLandscape} />
      <Button title="縦向きにロック" onPress={lockPortrait} />
      <Button title="画面の向きを解除" onPress={unlockOrientation} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  text: {
    fontSize: 18,
    marginBottom: 20,
  },
});

export default ScreenOrientationPage;

位置情報を表示する

最後に、位置情報を表示する機能を作っていきます。

こちらも同様に必要なライブラリをインストールし、

npm install expo-location

_layout.tsxに新しいタブを作成します。

<Tabs.Screen
  name="locationPage"
  options={{
    title: 'Location',
    tabBarIcon: ({ color }) => <IconSymbol size={28} name="location" color={color} />,
  }}
/>

そして、app>(tabs)配下にlocationPage.tsxファイルを作成します。
Expoのドキュメントに従い、位置情報の権限をリクエストするrequestForegroundPermissionsAsync、位置情報の緯度経度を取得するgetCurrentPositionAsync、緯度経度から住所に変換するreverseGeocodeAsyncを使います。

import { requestForegroundPermissionsAsync } from 'expo-location';

let { status } = await requestForegroundPermissionsAsync();
import { getCurrentPositionAsync, reverseGeocodeAsync } from 'expo-location';

const userLocation = await getCurrentPositionAsync({});
setLocation({
  latitude: userLocation.coords.latitude,
  longitude: userLocation.coords.longitude,
});

const geocode = await reverseGeocodeAsync({
  latitude: userLocation.coords.latitude,
  longitude: userLocation.coords.longitude,
});

これらを使用して作成したものがこちらです↓

loc1.gif

位置情報許可画面

位置情報が許可されてない場合
loc2.gif

ソースコードの全量はこちら↓

LocationPage.tsx
import React, { useState, useEffect } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import * as Location from 'expo-location';

const LocationPage = () => {
  const [location, setLocation] = useState<{ latitude: number; longitude: number } | null>(null);
  const [errorMsg, setErrorMsg] = useState<string | null>(null);
  const [address, setAddress] = useState<string | null>(null);

  const getLocation = async () => {
    // 位置情報の権限をリクエスト
    let { status } = await Location.requestForegroundPermissionsAsync();
    if (status !== 'granted') {
      setErrorMsg('位置情報のアクセス権限が必要です');
      return;
    }

    try {
      // 位置情報取得
      const userLocation = await Location.getCurrentPositionAsync({});
      setLocation({
        latitude: userLocation.coords.latitude,
        longitude: userLocation.coords.longitude,
      });

      // 住所取得
      const geocode = await Location.reverseGeocodeAsync({
        latitude: userLocation.coords.latitude,
        longitude: userLocation.coords.longitude,
      });

      // 住所情報の取得と整形
      if (geocode.length > 0) {
        const { country, region, city, street } = geocode[0];
        const fullAddress = `${country} ${region} ${city} ${street}`;
        setAddress(fullAddress);
      }
    } catch (error) {
      setErrorMsg('位置情報の取得に失敗しました');
    }
  };

  useEffect(() => {
    getLocation();
  }, []);

  return (
    <View style={styles.container}>
      <Text style={styles.title}>現在位置情報</Text>

      {errorMsg ? (
        <Text style={styles.errorText}>{errorMsg}</Text>
      ) : location ? (
        <View>
          <Text style={styles.text}>緯度: {location.latitude}</Text>
          <Text style={styles.text}>経度: {location.longitude}</Text>
          {address && <Text style={styles.text}>住所: {address}</Text>}
        </View>
      ) : (
        <Text style={styles.text}>位置情報を取得しています</Text>
      )}

      {/* 位置情報を再取得するボタン */}
      <Button title="位置情報を再取得" onPress={getLocation} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 20,
  },
  text: {
    fontSize: 18,
    marginBottom: 10,
  },
  errorText: {
    fontSize: 18,
    color: 'red',
    marginBottom: 10,
  },
});

export default LocationPage;

まとめ

React NativeとExpoを使うことで、モバイルアプリの開発を迅速に行うことができました。
普段からReactを扱っている開発者であれば、React Nativeは特別な言語の習得は必要なく、スムーズに開発を進められます。
また、Expoを活用することで、開発環境の作成が簡単になり、さらに多くの便利な機能が提供されているため、開発が一層効率的になります。

9
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?