Expo (React Native) でViewを画像に変換してデバイスに保存する

# はじめに

React Nativeで新作アプリを作りました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">久しぶりにカスみたいなアプリ作りました<br>姓名判断っぽい画像を捏造できます<br>12/30に完成したんですが、Appleさんに怪しい占いアプリと勘違いされ、なかなか出せませんでした<a href="https://twitter.com/hashtag/%E5%98%98%E5%A7%93%E5%90%8D%E5%88%A4%E6%96%AD?src=hash&amp;ref_src=twsrc%5Etfw">#嘘姓名判断</a><br><br>iOS<a href="https://t.co/2gUSUI2rQC">https://t.co/2gUSUI2rQC</a><br><br>Android<a href="https://t.co/IyslOklSkl">https://t.co/IyslOklSkl</a> <a href="https://t.co/kG0NxRcMkW">pic.twitter.com/kG0NxRcMkW</a></p>&mdash; yoshii🐍 (@ganja_tuber) <a href="https://twitter.com/ganja_tuber/status/1478724398152843272?ref_src=twsrc%5Etfw">January 5, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

嘘の姓名判断の結果を捏造できるおふざけアプリです。

好きなように運勢を選択して…
![65-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2088889/421709cd-e3db-1b93-edf6-58a4eea68997.png)

こんな感じの適当な姓名判断を作れます
![IMG_4899.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2088889/103199ea-c20d-fb15-efc9-e4ede1413d50.jpeg)

よかったら遊んでみてください。

# 困ったこと

React NativeでViewを画像に変換し、デバイスに保存するのに手間取ったので、
実装方法を書きます。

(Androidは実機を持っていないので、動かなかったらコメントとかで教えてください。一応シミュレーションでは動作確認してます。)

# GitHub リポジトリ

解説するコードは全て以下のリポジトリにまとめてます。

https://github.com/yoshiishunichi/ExpoConvertImageSample

# 実装方法

## 前提

Expo CLIを入れておいてください

https://docs.expo.dev/get-started/installation/

生のReact Native派の人は適宜読み替えてください
あと、yarn使います。npmの人も適宜読み替えてください。

## プロジェクト作成

まずは、expo init

ターミナルに以下を入力

```
expo init ExpoConvertImageSample
```

`ExpoConvertImageSample`のとこは適当に決めてください。

エンター押したらなんか聞かれるので、`blank (TypeScript)` を選びます。
TypeScriptじゃなかったり、react navigationしたい人は適宜変えてね。

## 実装

ライブラリ導入

```
yarn add react-native-view-shot expo-media-library
```

`react-native-view-shot`: Viewを画像に変換します

https://docs.expo.dev/versions/latest/sdk/captureRef/

`expo-media-library`: カメラロールに画像を保存します

https://docs.expo.dev/versions/latest/sdk/media-library/

以下コードです。

```App.tsx
import React, { useCallback, useRef, VFC } from "react";
import { View, Text, Button, Platform, Alert } from "react-native";
import ViewShot, { captureRef } from "react-native-view-shot";
import * as MediaLibrary from "expo-media-library";

const os = Platform.OS;

const App: VFC = () => {
  const viewShot = useRef(null);

  const permissionAlert = () => {
    Alert.alert(
      "画像の保存が許可されませんでした。",
      "設定から画像の保存を許可してください。"
    );
  };

  const saveImageFromView = async () => {
    try {
      const uri = await captureRef(viewShot);
      await MediaLibrary.saveToLibraryAsync(uri);
      Alert.alert("画像の保存完了");
    } catch (e) {
      console.log(e);
      permissionAlert();
    }
  };

  const capture = useCallback(async () => {
    // android
    if (os === "android") {
      const permission = await MediaLibrary.getPermissionsAsync();
      if (permission.canAskAgain && permission.status !== "granted") {
        const permissionResponse = await MediaLibrary.requestPermissionsAsync();
        if (permissionResponse.status !== "granted") {
          permissionAlert();
        } else {
          await saveImageFromView();
        }
      } else if (permission.status !== "granted") {
        permissionAlert();
      } else {
        await saveImageFromView();
      }
    }
    // iOS
    else {
      await saveImageFromView();
    }
  }, []);

  return (
    <View>
      <ViewShot
        ref={viewShot}
        style={{ height: 500, width: 300, backgroundColor: "red" }}
      >
        <Text style={{ fontSize: 30, marginTop: 200, textAlign: "center" }}>
          {"こんにちは〜"}
        </Text>
      </ViewShot>
      <Button title="変換して保存する" onPress={capture} />
    </View>
  );
};

export default App;
```

上記のコードをApp.tsxにコピペして、

```
yarn start
```

して、シミュレーター起動してボタンを押したらカメラロールに画像が保存されるはずです。

## 解説

やっていることの根幹は、`saveImageFromView`関数内の以下の2行です。
1行目でref属性に`viewShot`を指定しているViewをキャプチャーして、uriを取得。
2行目でそのuriから画像を保存してます。

```App.tsx
const uri = await captureRef(viewShot);
await MediaLibrary.saveToLibraryAsync(uri);
```
現在(2022/01/08)、iOSだとカメラロールに保存する処理が走ると初回には勝手に保存を許可するかユーザに尋ねるAlertが出てくれるので、ぶっちゃけこの2行だけでいいです。

iOSで許可を求める様子
![Simulator Screen Shot - iPhone 11 Pro Max - 2022-01-08 at 13.32.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2088889/2070635a-bbb8-7278-f544-ac3403c04817.png)

ただ、Androidだと少し話が変わってきます。

以下の1行でしっかり許可を要求する必要があります。

```App.tsx
const permissionResponse = await MediaLibrary.requestPermissionsAsync();
```

androidで許可を求める様子
![スクリーンショット 2022-01-08 14.37.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2088889/fed8ba65-8917-a5f7-5609-07a50981c793.png)

また、保存の許可を求めるAlertは何回も出しちゃいけないことになってるので、2回目以降は
`permissionAlert` 関数を使って設定から許可し直すことを要求するという形にしてます。

## 注意

開発環境でExpo Goを使って動作確認をしているときに、1回目の保存の許可を求めるAlertを拒否した場合、設定から再度許可しても保存できないようです。

1回目のAlertで保存を拒否した後の動作確認がしたい人は `yarn build`して、静的ファイルを生成して確認するのが確実で良いでしょう。

# まとめ

React NativeでViewを画像に変換して保存する方法を書きました。
これを使えば作れるアプリの幅が広がると思います。
わからないことや、問題があればコメントお願いします。

では、さようなら〜