production.log

株式会社リブセンスでエンジニアをやっている星直史のブログです。

ReactNativeでカメラロールから写真を複数枚選択する処理の実装

概要

自分が作っているプロダクトであるSnapmartは、スマホのアプリから誰でも手軽に写真を売り買いできるサービスです。
スマホアプリでは、カメラロールの中から写真をアップロードをする機能があります。
この写真をアップロードする機能ですが、カメラロールから写真を複数枚選択する処理の実装に非常に苦労しました。
この記事では、ReactNativeでカメラロールから写真を複数枚選択する処理をどのように実装しているのか / 実装の前に何を比較検討したのかを紹介します。

仕様の説明

まずSnapmartアプリにおける、写真の選択画面の仕様について、説明をします。
大まかな流れは以下の通りです。

  1. アップロードしたい写真があるアルバムを選択
  2. アルバムの中からアップロードしたい写真を複数選択

アルバム選択画面

f:id:watasihasitujidesu:20200213184903j:plain

こちらのアルバム選択画面の仕様は、以下の通りです。

  1. BottomNavigationの出品アイコンをタップするとスクリーンの表示
  2. カメラロール内の以下の項目を取得し表示(iOSの場合)
  3. 最近の項目
  4. お気に入り
  5. 存在するアルバム全て

写真選択画面

f:id:watasihasitujidesu:20200213184918j:plain

こちらの写真選択画面は以下の通りです。

  1. アルバム選択画面で選択したアルバム内の全ての写真を表示
  2. 表示される写真は撮影日の降順
  3. 複数枚選択し、アップロード

仕様だけなら非常にシンプルですね。

試してみたこと & 問題点

先述の仕様を実装するために、以下のことを試しました(実際にリリースもしました)。

  • アルバム選択画面を実装せず、直近1万枚の写真を表示し選択してもらう画面のみ実装
  • アルバム選択画面を実装せず、直近10万枚の写真を表示し選択してもらう画面のみ実装
  • ImagePickerを使用する

アルバム選択画面を実装せず、直近1万枚の写真を表示し選択してもらう画面のみ実装

元々のアプリはSwiftで実装されていたのですが、これをReactNativeに刷新しました。

blog.naoshihoshi.com

当初いち早くリリースするために、軽微(だと錯覚していた)機能は実装せずにリリースしてしまいました。
既存のユーザーさんからすると以下のような状態になってしまいました。

  • SwiftだろうがReactNativeだろうが、見た目は変わらない
  • それなのに、これまで使っていた機能が無くなってしまい不便になった

また、直近1万枚の写真しか表示されないので、1万件を超える写真が端末に保存されていた場合に画面に表示されない問題も発生しました。 これはテスト端末機に写真が1万枚も保存されていなかったため、事前に見抜くことができませんでした。

その結果、お問い合わせが急増してしまったり、アップロード率が低下してしまいました。

アルバム選択画面を実装せず、直近10万枚の写真を表示し選択してもらう画面のみ実装

お問い合わせが急増してしまったので、急いで修正する必要があります。 まず、1万枚の制限を10万枚に緩和してリリースしました。

しかし、10万枚を表示させたことによって、10万件の写真のソート処理に時間がかかってしまう問題に引っかかってしまいました。
基本的には、React NativeのCameraRoll APIを使用して実装をしています。 ところが、CameraRoll APIには、写真の並び順を指定するオプションが存在しないため、自前で実装する必要があります。 以下のようなイメージで実装します。

CameraRoll.getPhotos(options)
  .then((obj) => {
    let images = [];
    obj.edges.forEach(asset => {                                                                                 
      images.push(asset.node);
    })
    images.sort(function(a, b) {                                                      
      return b.timestamp - a.timestamp;
    })
  })

最大10万ループするので、写真を多く持つユーザーさんほど処理が遅くなってしまいます。
こちらでリリースした結果、以下のような問題点がありました。

  • ソートしている間は画面が真っ白になる
  • ソートにリソースが持っていかれるため
    • アプリがクラッシュしてしまう
    • 充電の減りが早い

ImagePickerを使用する

自前で実装しない方法を模索していたときに発見したのが、Expoが提供するImagePickerです。

ImagePicker - Expo Documentation

上記のページにある通り、このように実装するだけで、カメラロールからの写真選択が実装できます。
非常に素晴らしいAPIです。

ImagePicker.launchImageLibraryAsync

ただ、このAPIは写真を一枚ずつ選択しなければならないため、Snapmartにおける複数枚アップロードしたいという要望は満たすことができません。 試しにImageaPickerバージョンで社内メンバーに触ってもらったところ、見事にお蔵入りになりました。

悩んだ末の実装

結局、CameraRollで処理を頑張ることにしました。
また、CameraRoll.getPhotosにはオプションでgroupNameを指定することができます。
このgroupNameは写真が属しているアルバム名です。
そのため、Snapmartアプリにアルバム機能を実装して、CameraRoll.getPhotosに渡すオプションで、選択されたアルバム名をgroupNameで渡すことで、 ソートする写真を減らすというアプローチにしました。

ただ、アルバム名を取得しようにも、カメラロールに保存されている最後の1枚までアルバム名を取得しなければならず、結局ループする回数は減らすことはできませんでした。
苦肉の策として、以下を実装しています。

  • 一度取得したアルバム名(groupName)はAsyncStorageに格納する
  • AsyncStorageにデータがあった場合はそれを表示
  • アルバムを作成/削除した場合のために再読み込みボタンを設置
let folderNames = [];
CameraRoll.getPhotos(options)
  .then((obj) => {
    obj.edges.forEach(asset => {
      if(!folderNames.includes(asset.node.group_name)){
        folderNames.push(asset.node.group_name);
      }
    })
    AsyncStorage.setItem("hogehoge", folderNames.join(","));
  })

また、アルバム名(asset.node.group_name)で絞り込む場合はCameraRoll.getPhotosのオプションで指定します。

const options = {first: 99999, assetType: "Photos", groupTypes: "Album", groupName: "アルバム名"}
CameraRoll.getPhotos(options)

ReactNativeでカメラロールから写真を複数枚選択する場合のベストエフォート

  • カメラロールから写真を複数枚選択する
  • 写真の並び順を変更する

という要件は、簡単に実装できるように見えて、ReactNativeにおいては自前で実装が必要です。 また、CameraRoll APIは写真の並び順の指定ができないため、全ての写真をループで回し、timestampの降順/昇順、ファイル名の昇順/降順を決定する必要があります。

この問題点を少しでも緩和するために、写真選択画面の前に、アルバム選択画面を置くのが、今のところ最善策のように思えます。 理由は、アルバム選択をしてもらった場合、写真のループ処理の母集団が少なくなる(=それだけ処理が早く終わる)ためです。