wheatandcatの開発ブログ

React Nativeで開発しているペペロミア & memoirの技術系記事を投稿してます

ExpoとFirebase Storageで画像アップロード機能を実装

プロフィール画像のアップロード機能を実装したので記事にまとめた

Pull Request

github.com

実装

以下を使用して実装しました。

画像の選択からexpo-image-pickerを実装しました。

docs.expo.io

ライブラリから選択して画像を取得する場合はImagePicker.launchImageLibraryAsync、 カメラを起動して取った画像を取得する場合は、ImagePicker.launchCameraAsyncを使用します。

実装は以下の通りです。

components/organisms/UpdateProfile/ProfileImage.tsx

import React, { memo, useState, useCallback } from 'react';
import { StyleSheet, Alert, TouchableOpacity } from 'react-native';
import {
  useActionSheet,
  connectActionSheet,
} from '@expo/react-native-action-sheet';
import * as ImagePicker from 'expo-image-picker';
import View from 'components/atoms/View';
import Text from 'components/atoms/Text';
import theme from 'config/theme';
import { resizeImage } from 'lib/image';
import UserImage from 'components/molecules/User/Image';

export type Props = {
  image: string | null;
  onChangeImage: (uri: string) => void;
};

const ProfileImage: React.FC<Props> = (props) => {
  const [image, setImage] = useState<string | null>(props.image);
  const { showActionSheetWithOptions } = useActionSheet();

  const pickImageLibrary = useCallback(async () => {
    const mediaLibrary = await ImagePicker.requestMediaLibraryPermissionsAsync();
    if (mediaLibrary.status !== 'granted') {
      Alert.alert('memoirアプリのカメラのアクセス許可をONにしてください');
      return;
    }

    let result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.All,
      allowsEditing: true,
      allowMultipleSelection: false,
      aspect: [1, 1],
      quality: 1,
    });

    if (!result.cancelled) {
      const uri = await resizeImage(result.uri);
      props.onChangeImage(uri);
      setImage(uri);
    }
  }, [props]);

  const pickImageCamera = useCallback(async () => {
    const camera = await ImagePicker.requestCameraPermissionsAsync();
    if (camera.status !== 'granted') {
      Alert.alert('memoirアプリのカメラのアクセス許可をONにしてください');
    }

    let result = await ImagePicker.launchCameraAsync({
      allowsEditing: true,
      aspect: [1, 1],
      quality: 1,
    });

    if (!result.cancelled) {
      const uri = await resizeImage(result.uri);
      props.onChangeImage(uri);
      setImage(uri);
    }
  }, [props]);

  const onUpdateImage = useCallback(() => {
    showActionSheetWithOptions(
      {
        options: ['ライブラリから選択', '写真を撮る', 'キャンセル'],
        cancelButtonIndex: 2,
      },
      (buttonIndex) => {
        if (buttonIndex === 0) {
          pickImageLibrary();
        } else if (buttonIndex === 1) {
          pickImageCamera();
        }
      }
    );
  }, [showActionSheetWithOptions, pickImageLibrary, pickImageCamera]);

  return (
    <View style={styles.root}>
      <View mt={5}>
        <UserImage image={image} />
        <View my={3}>
          <TouchableOpacity onPress={onUpdateImage}>
            <Text textAlign="center" size="sm">
              写真を変更
            </Text>
          </TouchableOpacity>
        </View>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  root: {
    backgroundColor: theme().color.background.main,
    alignItems: 'center',
  },
});

export default connectActionSheet(memo(ProfileImage));

上記で、以下の動作の実装は完了です。

プロフィール画像は、画質が重要でないのアップロード前にファイルのリサイズを行っています。 ファイルのリサイズは、expo-image-manipulatorを使用しました。

docs.expo.io

src/lib/image.ts

import * as ImageManipulator from 'expo-image-manipulator';
import * as FileSystem from 'expo-file-system';


export const resizeImage = async (uri: string): Promise<string> => {
  const result = await ImageManipulator.manipulateAsync(
    uri,
    [
      {
        resize: {
          width: 500,
        },
      },
    ],
    { compress: 0, format: ImageManipulator.SaveFormat.PNG }
  );

  const fileInfo = await FileSystem.getInfoAsync(result.uri);
  console.log('file-size:', fileInfo.size);

  return result.uri;
};

ちなみ、expo-file-systemを使用することで実際のファイルサイズを確認することができます。

docs.expo.io

最後は画像アップロードを実装。 画像のアップロードはFirebase Storageを使用。 実装は以下通りです。

src/lib/image.ts

import firebase from 'lib/system/firebase';
import 'lib/firebase';
import { v4 as uuidv4 } from 'uuid';


export const uploadImageAsync = async (uri: string): Promise<string> => {
  console.log('uri:', uri);

  const blob: any = await new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.onload = function () {
      resolve(xhr.response);
    };
    xhr.onerror = function (e) {
      console.log(e);
      reject(new TypeError('Network request failed'));
    };
    xhr.responseType = 'blob';
    xhr.open('GET', uri, true);
    xhr.send(null);
  });

  const ref = firebase.storage().ref().child(uuidv4());
  const snapshot = await ref.put(blob);

  blob.close();

  return await snapshot.ref.getDownloadURL();
};

諸々を実装して、以下のように動作しました

www.youtube.com