スマホの中で重要な機能の一つであるカメラ機能。React Nativeを使ってモバイルアプリを開発するのならカメラで撮影した画像を保存したいのではないでしょうか。本文書ではReact Nativeからスマホ搭載のカメラを使って撮影を行い、撮影した画像をそのままスマホ内部のストレージに保存する方法を確認していきます。

iPhoneの場合は写真アプリに撮影した画像が保存されます。
fukidashi

本文書ではReact Nativeを利用したアプリ開発にExpoを利用しており、Expoが利用できる環境が事前に構築済みであることを前提に進めています。macOSでのExpo環境の構築とReact Nativeの基礎を学習したい場合は下記の文書が参考になります。

React Nativeプロジェクトの作成

expoコマンドを利用してReact Nativeプロジェクトの作成を行います。expo initコマンドを実行するとテンプレートの選択を行うことができますが本文書では”blank”を選択します。プロジェクト名は任意なので好きな名前をつけてください。ここではreact-native-imageという名前をつけています。


 % expo init react-native-image

expo-cameraのインストール

ExpoのドキュメントでCameraを検索するとモバイルデバイスに搭載されているカメラを利用するためにexpo-cameraが利用できることがわかります。動作確認のためプラットフォームの互換性(Platform Compatibility)を確認するとAndroidもiPhoneもエミュレータではXが付いているので本文書では主にiPhoneにインストールしたExpo Goを利用します。

Expoのドキュメント上におけるCameraの説明
Expoのドキュメント上におけるCameraの説明

expo-cameraのインストールを行います。


 % expo install expo-camera

Expo Goのインストール

iPhoneで動作確認を行う場合はApple Storeを開いてExpo Goをインストールしてください。

expo clientのインストール
expo goのインストール

expo-cameraの動作確認

Expoのドキュメントにカメラを動作させるためのサンプルコードが掲載されているでそれを元に動作確認を行います。サンプルコードの中ではStyleSheetを利用するためstyle.containerなどが設定されていますがスタイルの設定は各自が行う必要があるのでstyle.XXXはすべて削除をしています。またCameraコンポーネントのタグの中にFlipボタンが入っていますがここではCameraタグの中から外側にFlipボタンを出しています。

React Nativeではカメラを含め、位置情報、写真やカレンダーなどにアクセスするた場合は必ずユーザの許可が必要となります。カメラを利用する場合は、requestPermissionsAsync関数を利用してユーザに利用許可の確認を行います。スマホでアプリを利用する場合に確認画面は何度も目にしていると思いますがこの後どのような画面が表示されるか確認します。useEffect Hookでカメラへのアクセス許可の確認を行っているのでアプリを開くと同時にアクセス許可画面が表示されます。ユーザが許可すると戻り値に含まれるstatusがgrantedになります。


import React, { useState, useEffect } from 'react';
import { Text, View, TouchableOpacity } from 'react-native';
import { Camera } from 'expo-camera';

export default function App() {
  const [hasPermission, setHasPermission] = useState(null);
  const [type, setType] = useState(Camera.Constants.Type.back);

  useEffect(() => {
    (async () => {
      const { status } = await Camera.requestPermissionsAsync();
      setHasPermission(status === 'granted');
    })();
  }, []);

  if (hasPermission === null) {
    return <View />;
  }
  if (hasPermission === false) {
    return <Text>No access to camera</Text>;
  }
  return (
    <View style={{ flex: 1 }}>
      <Camera type={type} style={{ flex: 1 }} />
      <View>
        <TouchableOpacity
          onPress={() => {
            setType(
              type === Camera.Constants.Type.back
                ? Camera.Constants.Type.front
                : Camera.Constants.Type.back
            );
          }}
        >
          <Text> Flip </Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}
Flipは裏返す、ひっくり返すという意味がありビデオチャットや自分自身を撮影する際に利用するフロントカメラと携帯の背面に搭載されたカメラを切り返すために利用します。
fukidashi

動作確認を行うためにexpo startコマンドを実行します。左下に表示されているQRコードを手元のIPhoneのカメラで撮影してExpo Goを起動します。

管理画面
管理画面

IPhoneからアクセスするとカメラのアクセスを許可するかどうかの画面が表示されます。許可する場合は”OK”をタッチしてください。許可しないを選択した場合はExpo Goの設定画面からアクセス許可を変更する必要があります。設定変更については本文書の最後に記載しています。

カメラへのアクセス許可確認
カメラへのアクセス許可確認

画面の下部にメッセージが表示されます。requestPermissionsAsyncはdeprecatedなのでrequestPermissionsAsyncからrequestCameraPermissionsAsyncに変更すると画面の下部に表示されているメッセージは消えます。

”OK”を押すと画面にはカメラが撮影している内容が表示されます。下部には”Flip”という文字が表示されているのタッチするとフロントカメラとバックカメラが切り替わります。

画面にカメラが撮影した内容が表示
画面にカメラが撮影した内容が表示

expo-cameraを利用してカメラを通して映された内容が画面上に表示できるようになりました。

外側のViewをSafeAreaViewに変更し、Flipをアイコンに変更します。Expoでアイコンを利用すためにはimportを行う必要があります。アイコン用のライブラリのインストール等は必要ありません。


import { Ionicons } from '@expo/vector-icons';
//略
<SafeAreaView style={{ flex: 1 }}>
  <Camera type={type} style={{ flex: 1 }} />
  <View
    style={{
      height: 60,
      justifyContent: 'space-evenly',
      alignItems: 'center',
      flexDirection: 'row',
    }}
  >
    <TouchableOpacity
      onPress={() => {
        setType(
          type === Camera.Constants.Type.back
            ? Camera.Constants.Type.front
            : Camera.Constants.Type.back
        );
      }}
    >
      <Ionicons name="ios-camera-reverse-sharp" size={40} color="black" />
    </TouchableOpacity>
  </View>
</SafeAreaView>

アイコンをタッチするとフロントと背面のカメラが切り替わります。

ボタンの設定とSafeAreaView
ボタンの設定とSafeAreaView

カメラで撮影を行う

カメラを利用することができるようになりましたが写真を撮影するためのボタンもないため写真撮影を行うことができません。最初に撮影用のボタンを追加します。


<SafeAreaView style={{ flex: 1 }}>
  <Camera type={type} style={{ flex: 1 }} />
  <View
    style={{
      height: 60,
      justifyContent: 'space-evenly',
      alignItems: 'center',
      flexDirection: 'row',
    }}
  >
    <TouchableOpacity
      style={{
        width: 40,
        height: 40,
        borderRadius: 50,
        borderWidth: 5,
        borderColor: 'black',
      }}
    />
    <TouchableOpacity
      onPress={() => {
        setType(
          type === Camera.Constants.Type.back
            ? Camera.Constants.Type.front
            : Camera.Constants.Type.back
        );
      }}
    >
      <Ionicons name="ios-camera-reverse-sharp" size={40} color="black" />
    </TouchableOpacity>
  </View>
</SafeAreaView>

Expo Goで確認するとカメラアイコンの横に撮影ボタンようの丸が表示されます。

撮影用ボタン追加
撮影用ボタン追加

撮影ボタンをタッチすると表示されている画像が取得できるようにonPressイベントを設定します。onPressイベントではtakePicture関数を設定します。



expo-cameraを利用してカメラで撮影するためにtakePictureAsyncメソッドを使います。takePictureAsyncメソッドではて撮影した写真はアプリのキャッシュに保存します。保存の方法についてはドキュメントに記載がありrefを利用しているのでここでも同様の方法で設定を行います。

変数cameraをuseStateで定義します。


const [camera, setCamera] = useState(null);

Cameraタグにrefを設定します。


<Camera
  type={type}
  style={{ flex: 1 }}
  ref={(ref) => {
    setCamera(ref);
  }}
/>

takePicture関数を追加します。撮影ボタンをタッチすると処理が行われコンソールにキャッシュに保存された写真の情報が表示されます。


const takePicture = async () => {
  if (camera) {
    const image = await camera.takePictureAsync();
    console.log(image)
  }
};

画面から撮影ボタンをタッチすると画像のuriとheightとwidthの情報がObjectで表示されます。


Object {
  "height": 3720,
  "uri": "file:///var/mobile/Containers/Data/Application/4FFBEEEC-3D21-459A-9751-46DC0E337EE3/Library/Caches/ExponentExperienceData/%2540anonymous%252Freact-native-image-ae97eae2-174d-4a55-a8fc-b612c02554a3/Camera/D16E6882-7B27-414E-B7B4-1EE202570502.jpg",
  "width": 2376,
}

カメラを利用して撮影ができるようになりました。

撮影した画像を表示する

アプリのキャッシュの保存先がuriだということが確認できたのでこのuriとImageコンポーネントを利用して画面上に撮影した画像を表示させます。

撮影した画像の情報を保存するためにuseStateでpictureを定義します。


const [picture, setPicture] = useState(null);

撮影後のuriをpictureに保存します。


const takePicture = async () => {
  if (camera) {
    const image = await camera.takePictureAsync();
    setPicture(image.uri);
  }
};

pictureの中に値が保存されている場合は撮影した画像を表示し、値がない場合はカメラで撮影している内容を表示するように分岐を追加します。react-nativeからのImageのimportも忘れずに行ってください。


<SafeAreaView style={{ flex: 1 }}>
  <View style={{ flex: 1 }}>
    {!picture ? (
      <Camera
        type={type}
        style={{ flex: 1 }}
        ref={(ref) => {
          setCamera(ref);
        }}
      />
    ) : (
      <Image source={{ uri: picture }} style={{ flex: 1 }} />
    )}
  </View>

撮影ボタンをタッチすると写真が撮影され、その後撮影した画像が画面に表示されます。撮影した画像を画面上に表示できるようになりました。

カメラでの撮影に再度戻れるように撮影した写真が表示されている場合は撮影ボタンではなくカメラのアイコンが表示されるように設定を行います。ここでもpictureの値を利用して値がある場合はカメラのアイコン、ない場合は撮影ボタンを表示するように設定しています。


<SafeAreaView style={{ flex: 1 }}>
  <View style={{ flex: 1 }}>
    {!picture ? (
      <Camera
        type={type}
        style={{ flex: 1 }}
        ref={(ref) => {
          setCamera(ref);
        }}
      />
    ) : (
      <Image source={{ uri: picture }} style={{ flex: 1 }} />
    )}
  </View>
  <View
    style={{
      height: 60,
      justifyContent: 'space-evenly',
      alignItems: 'center',
      flexDirection: 'row',
    }}
  >
    {picture ? (
      <TouchableOpacity onPress={() => setPicture(null)}>
        <Ionicons name="ios-camera-outline" size={40} color="black" />
      </TouchableOpacity>
    ) : (
      <TouchableOpacity
        onPress={() => takePicture()}
        style={{
          width: 40,
          height: 40,
          borderRadius: 50,
          borderWidth: 5,
          borderColor: 'black',
        }}
      />
    )}
    <TouchableOpacity
      onPress={() => {
        setType(
          type === Camera.Constants.Type.back
            ? Camera.Constants.Type.front
            : Camera.Constants.Type.back
        );
      }}
    >
      <Ionicons name="ios-camera-reverse-sharp" size={40} color="black" />
    </TouchableOpacity>
  </View>
</SafeAreaView>

撮影ボタンをタッチして画面に撮影した画像が表示されている間は画面下部にカメラのアイコンが2つ並びます。

カメラアイコンを表示
カメラアイコンを表示

撮影した画像を保存する

カメラを使って撮影すること、撮影した画像を画面に表示することまでできるようになりました。最後にローカルに画像を保存する方法を確認します。

画像をローカルストレージに保存するためにexpo-media-libraryをインストールします。


 % expo install expo-media-library

expo-media-libraryを利用するためにimportを行います。


import * as MediaLibrary from 'expo-media-library';

カメラを利用するためにユーザの許可が必要であったように画像をローカルに保存する場合にもユーザの許可が必要になります。許可(permission)の確認にはMediaLibrary.requestPermissionsAsync()、保存にはMediaLibrary.createAssetAsyncを利用します。撮影後にそのまま保存できるようにtakPicture関数に保存のコード追加しています。


const takePicture = async () => {
  if (camera) {
    const image = await camera.takePictureAsync();
    setPicture(image.uri);
    const { status } = await MediaLibrary.requestPermissionsAsync();
    if (status === 'granted') {
      const asset = await MediaLibrary.createAssetAsync(image.uri);
    }
  }
};

撮影ボタンをタッチすると写真へのアクセスの確認が表示されます。すべての写真へのアクセスを許可をタッチすると画像は保存されます。

写真へのアクセスの許可確認
写真へのアクセスの許可確認

撮影した画像が画面に表示され保存されているかどうかはわからないので本当に保存されているかどうかは写真アプリを利用して確認してください。写真アプリの中に撮影した画像が存在するはずです。

もしPermissionの設定、grantedの確認を行わずにMediaLibrary.createAssetAsyncで画像を保存しようとした場合にはコンソールにエラーが表示されます。


[Unhandled promise rejection: Error: MEDIA_LIBRARY permission is required to do this operation.]

ここまでの動作確認でReact Nativeを利用して画像の保存を行うことができました。

Expo Goの許可の確認

間違ってアクセス許可の画面で許可しないを選択した場合または許可しないに変更したいといった場合はExpo Goの設定から変更を行う必要があります。iPhoneの設定からExpo Goアプリを選択して、下記の画面を表示してください。一度アプリ上で設定を行ったらここで設定変更を行うことになります。カメラをOFFにするとカメラが利用できなくなります。

Expo Goの設定画面
Expo Goの設定画面

デフォルトでは下記の設定になっており、アプリ上で許可等の設定を行っていくとそれぞれの機能におけるアクセス許可の項目が増えていきます。本文書ではカメラと写真の許可を行っているので2つの項目が増えています。

Expo Goのデフォルトの設定値
Expo Goのデフォルトの設定値

他の方法があるかもしれませんが再度画面上で許可の確認画面を表示したい場合はExpo Goを再インストールすることで可能になりました。