LoginSignup
39
37

More than 3 years have passed since last update.

React Native(Expo)で画像をFirebase StorageとFirestoreで管理する

Posted at

本記事について(ゴール)

こんにちはmatinanaです。技術記事人生初執筆です。宜しくお願いします。

本記事では、下記GIFのようにReact Native(Expo)を用いてフォトライブラリの画像をFirebase Storageに保存し、ストレージのダウンロードURLと画像のキャッチコピーをFirestoreで管理するまでの一連の実装をしてみます。


はじめに

主なフレームワーク等のバージョンについて

2019年9月11日現在最新(SDK34)のExpoを使っています。
"expo": "^34.0.1",
"react": "16.8.3",
"firebase": "^6.4.0",

ソースコードの記述について

この記事では要所のみを説明しています。そのため、記事内のソースコードは全文ではありません。実際に手を動かしてみる場合は、こちらのソースコードをコピーして使うというよりも、Githubの方を参照お願いします。


実装の流れ

  1. コンポーネントを作る
  2. imagePickerを使ってフォトライブラリの画像を取得する
  3. Firebaseをプロジェクトに導入する
  4. フォトライブラリから取得した画像をFirebese Storageに保存し、保存した画像のダウンロードURLをFirestoreと紐付ける

1. コンポーネントを作る

まずは外観を作るところから始めましょう。
と、言っても複雑な見た目は作っておらず、要素は3つのみです。

・PostScreen.js: モーダルを表示するボタンと追加された投稿を一覧で表示するスクリーン
・AddPostModa.js: 写真を追加するための登録フォーム(モーダル)
・Post.js: それぞれの投稿のリスト

screens/PostScreen.js
import React from 'react';
import {
  StyleSheet,
  View,
  Dimensions,
  KeyboardAvoidingView,
} from 'react-native';
import { Button } from 'react-native-elements';
import Modal from 'react-native-modal';
import Post from '../components/Post';
import AddPostModal from '../components/AddPostModal';

// 略

render() {
    return (
      <View style={styles.container}>
        {/* モーダルを表示するボタン */}
        <Button
          containerStyle={styles.addPostBtnContainer}
          buttonStyle={styles.addPostBtn}
          title="写真を追加"
          onPress={this.togglePostModal}
        />
        {/* モーダル */}
        <Modal
          isVisible={this.state.isAddPostModalVisible}
          deviceWidth={deviceWidth}
          deviceHeight={deviceHeight}
          animationIn="fadeIn"
          animationInTiming={300}
          style={styles.modal}
        >
          <Button
            buttonStyle={styles.cancelBtn}
            titleStyle={styles.cancelBtnText}
            title="キャンセル"
            onPress={this.togglePostModal}
          />
          <KeyboardAvoidingView behavior="padding" keyboardVerticalOffset={120}>
            <AddPostModal
              updateAddedPostState={this.updateAddedPostState}
              togglePostModal={this.togglePostModal}
            />
          </KeyboardAvoidingView>
        </Modal>
        {/* 投稿写真を表示するリスト */}
        <Post allPosts={this.state.allPosts} />
      </View>
    );
  }
}

react-native-elements

モーダルを表示するボタンに関しては、react-native-elementsを使っています。
ボタン、アイコン、リストなど使いやすいエレメントが用意されています。

react-native-modal

モーダルにはreact-native-modalを使っています。
react native標準のモーダルよりもカスタマイズがしやすく使いやすいです。

components/AddPostModal.js
import React from 'react';
import {
  StyleSheet,
  View,
  Alert,
  TouchableOpacity,
  Image,
  Dimensions,
} from 'react-native';
import { Button, Icon } from 'react-native-elements';
import { Fumi } from 'react-native-textinput-effects';
import { FontAwesome } from '@expo/vector-icons';  

// 略

render() {
    return (
      <View style={styles.container}>
        <TouchableOpacity
          style={styles.imageContainer}
          onPress={() => this.onAddImagePressed()}
        >
          {this.state.imgUrl ? (
            <Image style={styles.image} source={{ uri: this.state.imgUrl }} />
          ) : (
            <Icon
              name="camera-retro"
              type="font-awesome"
              size={50}
              containerStyle={styles.cameraIcon}
              color="gray"
            />
          )}
        </TouchableOpacity>
        {/* react-native-textinput-effectsでテキストインプットにアニメーションを実装 */}
        <Fumi
          label={'キャッチコピーをつけてね'}
          iconClass={FontAwesome}
          iconName={'hashtag'}
          iconColor={'#f4d29a'}
          inputPadding={16}
          inputStyle={{ color: '#444' }}
          labelStyle={{ color: '#ddd' }}
          style={styles.textContainer}
          onChangeText={phrase => this.setState({ phrase })}
          value={this.state.phrase}
        />
        <Button
          buttonStyle={[
            styles.addPostBtn,
            { display: this.state.imgUrl ? 'flex' : 'none' },
          ]}
          title="追加"
          onPress={() => {
            if (this.state.phrase.length >= 1) {
              this.onPressAdd();
            } else {
              Alert.alert('キャッチコピーを入力してね', '');
            }
          }}
        />
      </View>
    );
  }
}

react-native-textinput-effects

登録フォームのテキストインプットではreact-native-textinput-effectsというテキストインプットにおしゃれなアニメーションをつけられるライブラリを使っています。
数種類のアニメーションがあるのでぜひライブラリのページを見てください。

components/Post.js
import React from 'react';
import { Text, Image, StyleSheet, Dimensions, FlatList } from 'react-native';
class Post extends React.Component {
  renderPost({ item }) {
    return (
      <React.Fragment>
        <Image style={styles.image} source={{ uri: item.imgUrl }} />
        <Text style={styles.phrase}>#{item.phrase}</Text>
      </React.Fragment>
    );
  }
  render() {
    return (
      <FlatList
        contentContainerStyle={{ alignItems: 'center' }}
        // 上から投稿順に表示
        data={[...this.props.allPosts].sort(
          (a, b) => b.postIndex - a.postIndex,
        )}
        keyExtractor={item => item.postIndex}
        renderItem={this.renderPost}
      />
    );
  }
}

Post.jsはシンプルにFlatlistで投稿を表示しているだけです。
React.Fragmentに関してご存知ない方はこちらの記事を参照してください。QiitaでReactNative・Expo関連の記事を多く執筆してくださっている@kabaさんの記事です。


2. imagePickerを使ってフォトライブラリの画像を取得する

ExpoのimagePickerを使ってイメージライブラリから画像を引っ張ってきます。
その後、ImageManipulatorを使って画像をリサイズしています。

components/addPostModal.js
// Permissionsなどのimportの書き方がExpoの最新バージョンでは以前と異なっているので要確認
// SDK32までは下記のようにExpoから直接読み込めた
// import { Permissions, ImagePicker, ImageManipulator } from 'expo' 

// SDK33からはそれぞれ個別に読みこむようになっている
import * as Permissions from 'expo-permissions';
import * as ImagePicker from 'expo-image-picker';
import * as ImageManipulator from 'expo-image-manipulator';

class AddPostModal extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      imgUrl: '',
      phrase: '',
      addedPost: [],
    };
  }

onAddImagePressed = async () => {
    const { status } = await Permissions.askAsync(Permissions.CAMERA_ROLL);
    if (status === 'granted') {
      const result = await ImagePicker.launchImageLibraryAsync({
        allowsEditing: true,
        mediaTypes: ImagePicker.MediaTypeOptions.Images,
      });
      if (!result.cancelled) {
        // ImageManipulatorでリサイズ処理
        const actions = [];
        actions.push({ resize: { width: 350 } });
        const manipulatorResult = await ImageManipulator.manipulateAsync(
          result.uri,
          actions,
          {
            compress: 0.4,
          },
        );
        this.setState({
          imgUrl: manipulatorResult.uri,
        });
      }
    }
  };

3. Firebaseをプロジェクトに導入する

Firebaseでグーグルログインした後に、「プロジェクトを追加」から新規プロジェクトを立ち上げます。プロジェクトの名前とGoogle アナリティクスの設定(今回は無効に変更)の2つだけの設定でプロジェクトが作成できます。

今回はデータベースとしてFirestoreを利用し、画像はStorageに保存するため、それらの設定も含め以下の4つの設定をFirebaseで行います。
3-1. Firebaseをアプリに追加する
3-2. 匿名認証の設定をする
3-3. Firestoreの設定をする
3-4. Storageの設定をする

3-1. Firebaseをアプリに追加する

まずは上記のProject Overviewからオレンジの丸で囲った「ウェブアプリへの追加」を選択し、アプリのニックネームを入力しアプリを登録します。
この際「このアプリをFirebase Hostingも設定します」という項目もありますが、今回はチェックを入れず無視して大丈夫です。

アプリを登録すると上記のようなfirebaseConfigの情報が表示されます。
こちらは後ほど使うのでどこかにコピペしておいてください。
現在はまだstorageBucketの部分が空欄だと思いますが、後ほどStorageの設定を行った際に追記します。

3-2 匿名認証の設定をする

左のタブからAuthenticationを選択し「ログイン方法を設定」を選びます。
ログインプロバイダの「匿名」を選択し「有効にする」にチェックを入れて保存します。

3-3. Firestoreの設定をする

左のタブからDatabaseを選択。

上記のデータベースの作成を選択。
セキュリティに関する選択肢が出てくるので、今回は「テストモードで開始」を選択。
Cloud Firestoreのロケーションを選択する画面に切り替わるので、ロケーションを「asia-northeast1」に選択して完了。
*ロケーションに関して詳しく知りたい方は公式サイトのドキュメントをお読みください。

3-4. Storageの設定をする

左のタブからStorageを選択。
「スタートガイド」を選択するとセキュリティの画面が表示されるのでそのまま「次へ」→「完了」。

上の画面に遷移すると思いますので、下記オレンジの丸で囲った部分の情報を先程メモしたfirebaseConfigのstorageBucketへ記述してください。

これでFirebase側での一連の設定は終わりになります。


4. フォトライブラリから取得した画像をFirebese Storageに保存し、保存した画像のダウンロードURLをFirestoreと紐付ける

まずはプロジェクトにFirebaseをインストールしましょう。

$ npm install firebase

Firebase関連のコードはFire.jsにまとめて記述する形式にします。
uploadPost以外の実装に関してはこちらのサイトを参考にしているので、こちらを読んでいただいた方がよいかと思います。(maricuruさん大変参考になりました。ありがとうございます。)

utils/Fire.js
import firebase from 'firebase';
import 'firebase/firestore';
class Fire {
  constructor() {
    // Githubのコードではconfig.jsとして別ファイルから読み込んでいます
    firebase.initializeApp({
      // ここに先程メモしたFirebaseConfigの内容を記述
      apiKey: '***',
      authDomain: '***',
      databaseURL: '***',
      projectId: '***',
      storageBucket: '***',
      messagingSenderId: '***',
      appId: '***',
  });
    // 匿名認証
    firebase.auth().onAuthStateChanged(user => {
      if (!user) {
        firebase.auth().signInAnonymously();
      }
    });
  }

  // 投稿時の処理
  uploadPost = async ({ url, phrase, postIndex }) => {
    const uploadRef = await this.postCollection.doc(postIndex);
    uploadRef
      .set({
        imgUrl: url,
        phrase,
        postIndex,
      })
      .then(() => {
        console.log('書き込みができました');
      });
  };

  // Firestoreに保存した情報をPostScreenで取得し、setStateする際の処理
  getPosts = async () => {
    const querySnapshot = await this.postCollection.get();
    const res = [];
    querySnapshot.forEach(doc => {
      res.push(doc.data());
    });
    return res;
  };

  get userCollection() {
    return firebase.firestore().collection('users');
  }
  get postCollection() {
    return this.userCollection.doc(this.uid).collection('posts');
  }
  get uid() {
    return (firebase.auth().currentUser || {}).uid;
  }
}
Fire.shared = new Fire();
export default Fire;

各コンポーネントのファイルでもFirebase関連の記述をします。
ストレージへの画像の保存は公式ドキュメントのこちらを、Firestoreへのデータの追加はこちらを参照しながら実装しています。

components/addPostModal.js
import firebase from 'firebase';
import Fire from '../utils/Fire';
class AddPostModal extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      imgUrl: '',
      phrase: '',
      addedPost: [],
    };
  }
  // 投稿時の処理
  async onPressAdd() {
    await this.uploadPostImg();
    const { imgUrl, phrase, postIndex } = await this.state;
    this.uploadPost(imgUrl, phrase, postIndex);
    this.setState(
      {
        addedPost: [
          {
            imgUrl,
            phrase,
            postIndex,
          },
        ],
      },
      () => this.updateAddedPostState(),
      this.props.togglePostModal(),
    );
  }

  onAddImagePressed = async () => {
    // 略
  };

  uploadPostImg = async () => {
    const metadata = {
      contentType: 'image/jpeg',
    };
    const postIndex = Date.now().toString();
    const storage = firebase.storage();
    const imgURI = this.state.imgUrl;
    const response = await fetch(imgURI);
    const blob = await response.blob();
    const uploadRef = storage.ref('images').child(`${postIndex}`);

    // storageに画像を保存
    await uploadRef.put(blob, metadata).catch(() => {
      alert('画像の保存に失敗しました');
    });

    // storageのダウンロードURLをsetStateする
    await uploadRef
      .getDownloadURL()
      .then(url => {
        this.setState({
          imgUrl: url,
          postIndex,
        });
      })
      .catch(() => {
        alert('失敗しました');
      });
  };

  // stateに入っているダウンロードURLなどをFirestoreに記述する
  uploadPost(url, phrase, postIndex) {
    Fire.shared.uploadPost({
      url,
      phrase,
      postIndex,
    });
  }

  // PostScreen.jsで投稿データのstateを管理する
  updateAddedPostState() {
    this.props.updateAddedPostState(this.state.addedPost);
  }

  render() {
    // 略
  }
}
screens/PostScreen.js
import firebase from 'firebase';
import Fire from '../utils/Fire';
import Post from '../components/Post';
import AddPostModal from '../components/AddPostModal';

class PostsScreen extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      allPosts: [],
      isAddPostModalVisible: false,
    };
    //  Firestoreのデータを読みこむ 
    this.downloadAllPosts();
  }

  // addPostModal.jsで追加した投稿をchildStateとして受け取り、PostScreenで全投稿を管理する
  updateAddedPostState = childState => {
    this.setState({
      allPosts: [...this.state.allPosts, ...childState],
    });
  };

  async downloadAllPosts() {
    firebase.auth().onAuthStateChanged(async user => {
      if (user) {
        const posts = await Fire.shared.getPosts();
        this.setState({
          allPosts: posts,
        });
      }
    });
  }

  render() {
  // 略
  }
}

一連の流れの確認

  1. モーダルで画像とキャッチコピーを入力し追加ボタンを押すとonPressAdd()が始まる
  2. onPressAdd()で以下の処理が走る
    • uploadPostImg()の処理でFirebase Storageへの画像のアップロードが完了し、その後画像のダウンロードURLがaddPostModalのstateに書き込まれる
    • uploadPost()の処理でstateの情報がFirestoreに書き込まれる
    • updateAddedPostState()でaddPostModalで投稿した情報をPostScreenに集約し、Postに流す
  3. 投稿がPost.jsのFlatListで描画される

以後Firestoreに投稿内容がある場合は、downloadAllPosts()で以前Firestoreに追加した情報がPostScreenにsetStateされることで、アプリを再起動した時などでも追加したデータが画面に描写されることになっています。


最後に

今回は削除機能などを実装しなかったため、以上で実装は終わりになります。
上記の実装はとりあえず動く形での実装になっており、改善点等もあるかと思います。
Expo、React Nativeに関する情報はまだまだネット上で多くはないため、動く実装の一つとして記事投稿いたしました。もし改善案などありましたらご提案頂ければ幸いです。

Happy Hacking! \(^o^)/

39
37
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
39
37