5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

KIT DeveloperAdvent Calendar 2019

Day 5

expo-cameraで撮った画像をfirebaseのstrageに保存する

Posted at

はじめに

友達がアースノーマットをアレクサと呼んでいたことを1週間後くらいに思い出して笑いました。その時は何も反応しなかったのですが、明日1週間の時を経て適当に突っ込んでおきたいと思います。
はい、ということで?今回はexpo-cameraでとった画像をfirebaseのstrageに保存していきます。

expo

expoはreact-nativeやネイティブプラットフォームを中心に構築されたツールとサービスです。javascriptやtypescriptからios,android,webアプリの開発をすることができ、expo独自のワークフローでアプリの構築からデプロイまで面倒を見てくれます。
react-nativeでアプリを作る際にexpo-cliで開発する場合とreact-native-cliで開発する場合がありますが、初めて react-nativeでアプリ開発する方はexpo-cliを使った方がいいと思います。
expoで開発をする際はiosなどのネイティブ機能を使うことはできず、expoで提供されている機能しか使うことができません。ある程度はexpo側でネイティブ機能(プッシュ通知など)を用意してくれているため、個人開発で使いたい機能がなくて困る場面は少ないと思います。ここからexpoで使うことのできる機能を見ることができます。
また、expoでスタートしてある程度アプリを作り込んでからネイティブ機能を使いたくなった場合も問題ありません。ejectという作業をすることでネイティブ機能使い放題となります。

expoでfirebaseを使う

expoでfirebaseを使う場合はFirebase JS SDKを使用します。このSDKからfirestoreやstrageなどを使うことができます。少し注意しなければならないことは、Authenticationを使う場合にwebのコンポーネントは使えません。(Facebook認証はexpoが素晴らしいコンポーネントを用意してくれています。)
今回はfirebaseのセキュリティルールは無視してとりあえずstorageに写真を保存します。firebaseのコンソールからstargeのルールをこんな感じ👇にしておきます。

service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write;
    }
  }
}

本題へ

expoが提供している機能の一つであるexpo-cameraを使いスマホのカメラから写真をとります。全体的なコンポーネントはこんなんです。ちなみに最近TSを始めたため、型の付け方は正直適当です。TSLintでエラー出なくなったから大丈夫なのか?程度です。

Camera.tsx
import React, { useState, useEffect, useRef } from 'react';
import { NavigationStackScreenComponent } from 'react-navigation-stack';
import { View, StyleSheet } from 'react-native';
import {
  Button, Container, Icon, Text,
} from 'native-base';
import { Camera as ExpoCamera } from 'expo-camera';
import * as Permissions from 'expo-permissions';
import { upLoadImg } from '../../../utils/upLoadImg';

const styles = StyleSheet.create({
  button: {
    position: 'absolute',
    bottom: 100,
    zIndex: 1,
    alignSelf: 'center',
    height: 80,
    width: 80,
    flex: 1,
    justifyContent: 'center',
  },
  icon: {
    fontSize: 50,
  },
  flexOne: {
    flex: 1,
  },
});

const Camera: NavigationStackScreenComponent = () => {
  const [cameraPermission, setCameraPermission] = useState<null|boolean>(null);

  const permission = async (): Promise<void> => {
    const { status } = await Permissions.askAsync(Permissions.CAMERA);
    const pms = (status === 'granted');
    setCameraPermission(pms);
  };
  useEffect(() => {
    permission();
  }, []);

  const cameraRef = useRef(null);

  const snap = async () => {
    if (cameraRef) {
      const { uri } = await cameraRef.current.takePictureAsync(); // uriはローカルイメージURIで一時的にローカルに保存される
      const response = await fetch(uri);
      const blob = await response.blob();
      const imgName = blob.data.name;
      // console.log(blob.data.name);
      upLoadImg(imgName, blob).then((url) => { console.log(url); });
    }
  };

  const renderCamera = (): React.ReactElement => {
    // console.log(cameraPermission);
    if (cameraPermission === null) {
      return <View />;
    } if (cameraPermission === false) {
      return <Text>No access to camera</Text>;
    }
    return (
      <Container style={styles.flexOne}>
        <ExpoCamera style={styles.flexOne} ref={cameraRef}>
          <View style={styles.flexOne}>
            <Button
              rounded
              icon
              onPress={() => snap()}
              style={styles.button}
            >
              <Icon name="camera" style={styles.icon} />
            </Button>
          </View>
        </ExpoCamera>
      </Container>
    );
  };

  return (
    <>{renderCamera()}</>
  );
};

export default Camera;

肝心の画像をstrageに保存するメソッドはこれ👇です。

upLoadImg.tsx
import firebase from './initializeFirebase';

export const upLoadImg = (imgName, blob): Promise<string> => new Promise((resolve) => {
  const storageRef = firebase.storage().ref();
  const cloudStoragePath = storageRef.child(imgName);
  cloudStoragePath.put(blob).then((snapshot): void => {
    cloudStoragePath.getDownloadURL().then((url) => {
      console.log('ok');
      resolve(url);
    }).catch((error) => {
      console.log(error);
    });
  });
});

firebaseの初期化はこれ👇です

initializeFirebase.tsx
import * as firebase from 'firebase';
import 'firebase/storage';

const firebaseConfig = {
  apiKey: 'hoge',
  authDomain: 'huga',
  databaseURL: 'hana',
  projectId: 'hoji',
  storageBucket: 'hoji',
  messagingSenderId: 'huga',
  appId: 'hoge',
};

firebase.initializeApp(firebaseConfig);

export default firebase;

ポイント

  • ボタンを押すとstrageに保存された画像のURLをコンソール出力しています
  • カメラを使う場合は最初にカメラのパーミッション確認が必要です
  • permission()はマウントされた時一回実行されれば良いのでuseEffectの第二引数に空の配列を渡します
  • expo-cameraでとった画像は一時的にデバイスに保存されます。保存されているuriにフェッチして画像を入手します
  • firebaseは結構いろんな画像形式をサポートしていますが今回はblob形式に変換します

permissionの部分をhooksに抽出してみる

permission()は引数は何もいりませんし、返している値も一つだけです。こうなりました👇

usePermission.tsx
import { useState, useEffect } from 'react';
import * as Permissions from 'expo-permissions';

const usePermission = () => {
  const [cameraPermission, setCameraPermission] = useState<null|boolean>(null);

  const permission = async (): Promise<void> => {
    const { status } = await Permissions.askAsync(Permissions.CAMERA);
    const pms = (status === 'granted');
    setCameraPermission(pms);
  };
  useEffect(() => {
    console.log('permission');
    permission();
  }, []);

  return {
    cameraPermission,
  };
};

export default usePermission;

そして、cameraコンポーネントはこうなります。

Camera.tsx
import React, { useRef } from 'react';
import { NavigationStackScreenComponent } from 'react-navigation-stack';
import { View, StyleSheet } from 'react-native';
import {
  Button, Container, Icon, Text,
} from 'native-base';
import { Camera as ExpoCamera } from 'expo-camera';
import { upLoadImg } from '../../../utils/upLoadImg';
import usePermission from '../../../utils/usePermission';

const styles = StyleSheet.create({
  button: {
    position: 'absolute',
    bottom: 100,
    zIndex: 1,
    alignSelf: 'center',
    height: 80,
    width: 80,
    flex: 1,
    justifyContent: 'center',
  },
  icon: {
    fontSize: 50,
  },
  flexOne: {
    flex: 1,
  },
});

const Camera: NavigationStackScreenComponent = () => {
  const { cameraPermission } = usePermission();

  const cameraRef = useRef(null);

  const snap = async () => {
    if (cameraRef) {
      const { uri } = await cameraRef.current.takePictureAsync(); // uriはローカルイメージURIで一時的にローカルに保存される
      const response = await fetch(uri);
      const blob = await response.blob();
      const imgName = blob.data.name;
      // console.log(blob.data.name);
      upLoadImg(imgName, blob).then((url) => { console.log(url); });
    }
  };

  const renderCamera = (): React.ReactElement => {
    // console.log(cameraPermission);
    if (cameraPermission === null) {
      return <View />;
    } if (cameraPermission === false) {
      return <Text>No access to camera</Text>;
    }
    return (
      <Container style={styles.flexOne}>
        <ExpoCamera style={styles.flexOne} ref={cameraRef}>
          <View style={styles.flexOne}>
            <Button
              rounded
              icon
              onPress={() => snap()}
              style={styles.button}
            >
              <Icon name="camera" style={styles.icon} />
            </Button>
          </View>
        </ExpoCamera>
      </Container>
    );
  };

  return (
    <>{renderCamera()}</>
  );
};

export default Camera;

そして次回へ

カレンダーの空きが多いので分けます!
分けたら次の記事短くなるけど分けます!

参考・出典

5
1
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
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?