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

# はじめに
友達がアースノーマットをアレクサと呼んでいたことを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側でネイティブ機能(プッシュ通知など)を用意してくれているため、個人開発で使いたい機能がなくて困る場面は少ないと思います。[ここ](https://docs.expo.io/versions/v35.0.0/sdk/overview/)からexpoで使うことのできる機能を見ることができます。
また、expoでスタートしてある程度アプリを作り込んでからネイティブ機能を使いたくなった場合も問題ありません。ejectという作業をすることでネイティブ機能使い放題となります。

# expoでfirebaseを使う
expoでfirebaseを使う場合は[Firebase JS SDK](https://github.com/firebase/firebase-js-sdk)を使用します。このSDKからfirestoreやstrageなどを使うことができます。少し注意しなければならないことは、Authenticationを使う場合にwebのコンポーネントは使えません。（Facebook認証はexpoが[素晴らしいコンポーネント](https://docs.expo.io/versions/v35.0.0/sdk/facebook/#facebookloginwithreadpermissionsasyncappid-options)を用意してくれています。）
今回はfirebaseのセキュリティルールは無視してとりあえずstorageに写真を保存します。firebaseのコンソールからstargeのルールをこんな感じ👇にしておきます。

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

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

```typescript: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に保存するメソッドはこれ👇です。

```typescript: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の初期化はこれ👇です

```typescript: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は結構[いろんな画像形式をサポート](https://firebase.google.com/docs/storage/web/upload-files?hl=ja#upload_from_a_byte_array)していますが今回はblob形式に変換します


# permissionの部分をhooksに抽出してみる
permission()は引数は何もいりませんし、返している値も一つだけです。こうなりました👇

```typescript: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コンポーネントはこうなります。

```typescript: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;

```

## そして次回へ
カレンダーの空きが多いので分けます！
分けたら次の記事短くなるけど分けます！

## 参考・出典
- https://qiita.com/ryo2132/items/80536b6ea4ed9f1e8a08