【7】React Nativeでテキストエディタを作ってみる!【エクスポート・バックアップ編】

こんにちは!かたつむり(@Katatumuri_nyan)です!

Reactを触ってみて、サイト的なものは作れるようになりました(*´ω`)
そこで、次はReactNativeを触ってみようと思い、簡単なテキストエディタを作成しようと企んでおります(笑)

データの読み書きはできたので、今回はファイルとしてエクスポートしたいと思います!
バックアップもとれるようにしていけたらな~!

これができたら、見た目がすごくダサいので、デザインも変えていきたい…

よっしゃ!やっていきます!

最初から見る↓
【1】React Nativeでテキストエディタを作ってみる!【下調べ編】【1】React Nativeでテキストエディタを作ってみる!【下調べ編】

前回を見る↓
【6】React Nativeでテキストエディタを作ってみる!【データ管理編】【6】React Nativeでテキストエディタを作ってみる!【データ管理編】

今回やりたいこと

  • マークダウン・HTMLファイルとしてデータをエクスポート
  • ファイルを端末内のファイルに保存できるようにする
  • ファイルをGoogleドライブなどに共有できるようにする
  • バックアップを取れるようにする

ファイルとしてエクスポートして保存

データはJSONとして保存されているみたいです。
これ、メモ帳だけのアプリならいいですが、Markdownテキストエディタとしては不十分なので、ファイルとしてエクスポートしたいと思います(*´ω`)

Expo公式ドキュメントを参考にファイルシステムを導入していきます!

(エクスポートは拡張子指定して保存したら自動でしてくれるんだろうか?)

とりあえず、公式ドキュメント難しいので、やってみます!

documentDirectorycacheDirectoryが選べるみたいです。
一時ファイルではないので、documentDirectoryに保存するようにやっていきたいと思います。

import * as FileSystem from 'expo-file-system';でインポート。
よし、いけるぞ!

ディレクトリ・ファイルを作成する

makeDirectoryAsyncでディレクトリを作成できそうですね。
ディレクトリがない時は作成、あるときは作成しないにもoptionsでできそう。

ファイルを保存するメソッドがない…
androidあるけど…?
reactjs – React native(expo)でテキストファイルを作成する方法は?
↑これより、MediaLibraryでできそうな気がしてきました。
expo install expo-media-libraryします。

createAssetAsyncでいけるのかな?書いてみます。

とりあえず、ファイルも作成できるのか試してみます。

import * as FileSystem from 'expo-file-system';

function saveMdFile(){
  const fileUri = FileSystem.documentDirectory + 'SimpleMarkdown/myFile.md'
  FileSystem.makeDirectoryAsync(fileUri, true)

}
Possible Unhandled Promise Rejection

エラーが出ました!どういう意味…?
Promise Rejectionが拒否されたってことですかね。
とりあえず、処理はできなかったw

MediaLibrary.requestPermissionsAsync()
↑これでメディアライブラリへのアクセスの許可を撮ってからやってみます。

export default async function saveMdFile(){
  await MediaLibrary.requestPermissionsAsync()
  const fileUri = await FileSystem.documentDirectory + 'SimpleMarkdown'
  FileSystem.makeDirectoryAsync(fileUri, true)
}

↑とりあえず、ディレクトリを作成してみます。
(゜-゜)できない。
試行錯誤します!

export default async function saveMdFile(){
  await MediaLibrary.requestPermissionsAsync()
  const fileUri = await FileSystem.documentDirectory + 'SimpleMarkdown'
  await FileSystem.makeDirectoryAsync(fileUri, true).then(e=>{
    console.log(e);
  }).catch(err =>{
    console.error(err);
  })
}

↑こうしてみると↓のエラーが出ます。

ExceptionsManager.js:179 Error: An exception was thrown while calling `ExponentFileSystem.makeDirectoryAsync` with arguments `(
    "file://…省略",
    1
)`
FileSystem.makeDirectoryAsync(fileUri)

もしかして引数の書き方が違う?
↑に変えてみました。

> null

引数書き間違えみたいですね!ww
{ intermediates: true }こうでした!公式ドキュメントはよく読みましょう…

await MediaLibrary.requestPermissionsAsync()

↑ちなみに、これはいりませんでした。

さて、保存されているっぽいんですが、どこにあるんだろう…

FileSystem.readAsStringAsync(fileUri, options)

これで確認してみます。

Error: File 'file://…省略' could not be read.

ファイルないみたいですね!

FileSystem.readDirectoryAsync(fileUri)

これでディレクトリがあるか確認してみます。

picture 2
いるwwwww

FileSystem.makeDirectoryAsync()これでファイルも保存できるっぽいですね。

でもFileSystem.readAsStringAsync()で読み取れません。

中身がないからかもですね。
FileSystem.writeAsStringAsync()これで書き込んでみよう!

だめでしたw
書き込めません。

FileSystem.deleteAsync()これで消してみます。

あ、消せた。

export default async function saveMdFile(){
  const directoryUri = FileSystem.documentDirectory + 'SimpleMarkdown/'
  const fileUri = directoryUri + 'test.md'
  FileSystem.writeAsStringAsync(fileUri, "Hello World", { encoding: FileSystem.EncodingType.UTF8 })
    .then(e => {
      console.log("writeAsStringAsync >>" + e);
    }).catch(err => {
      console.error(err);
    })

  FileSystem.readAsStringAsync(fileUri, { encoding: FileSystem.EncodingType.UTF8 })
    .then(e => {
      console.log("readAsStringAsync >>" + e);
  }).catch(err => {
    console.error(err);
  })
  FileSystem.readDirectoryAsync(directoryUri)
    .then(e => {
      console.log("readDirectoryAsync >>"+ e);
    }).catch(err => {
      console.error(err);
    })

}

↑の実行結果が↓です。
picture 3

ファイル作成までできちゃった。
でもどこにいるのかイマイチ分からないですね…。

reactjs – React native(expo)でテキストファイルを作成する方法は?
↑より。
MediaLibrary.createAssetAsync()MediaLibrary.createAlbumAsync()でユーザーから見えるところにファイル作れそうですね。
作ってみます。

MEDIA_LIBRARY permission is required to do this operation.
↑エラーが出た!w

await MediaLibrary.requestPermissionsAsync()

権限の許可が必要なので、再び↑追加します。
(権限の再要求とかの機能も必要そうですね。)

うーん。

Error: This file type is not supported yet

↑のように出てしまいました。
MediaLibraryって、メディアライブラリへの保存だし、そもそも使うものが違うんでしょうね。

FileSystem.getContentUriAsync(fileUri)share(content, options)をしてみて、ファイルアプリで開いてみます。

picture 4
picture 5
picture 6

できたー!!やった(*´ω`)
普段はアプリ内に保存して、取り出したいときはエクスポートできる感じになりましたね!やった!

これならキャッシュフォルダ使えばよさそう。
うまいこと整形して以下の関数になりました~!

export async function exportMdFile(filename,content){
  const directoryUri = FileSystem.cacheDirectory + 'SimpleMarkdown/'
  const fileUri = directoryUri + filename

  await FileSystem.makeDirectoryAsync(directoryUri, { intermediates: true })
    .then(e=>{
      console.log("makeDirectoryAsync" + e);
  }).catch(err =>{
    console.error(err);
  })

  await FileSystem.writeAsStringAsync(fileUri, content, { encoding: FileSystem.EncodingType.UTF8 })
    .then(e => {
      console.log("writeAsStringAsync >>" + e);
    }).catch(err => {
      console.error(err);
    })

  await FileSystem.readAsStringAsync(fileUri, { encoding: FileSystem.EncodingType.UTF8 })
    .then(e => {
      console.log("readAsStringAsync >>" + e);
  }).catch(err => {
    console.error(err);
  })
  await FileSystem.readDirectoryAsync(directoryUri)
    .then(e => {
      console.log("readDirectoryAsync >>"+ e);
    }).catch(err => {
      console.error(err);
    })

  const shareUrl =await FileSystem.getContentUriAsync(fileUri)
  console.log(shareUrl);
  Share.share({url:shareUrl})
    .then(e => {
      console.log(Share.sharedAction);
    }).catch(err => {
      console.error(err);
    })

}

log吐き多いですが、デバッグのためにご容赦くださいw

日本語ファイル名などで保存できるようにする

今は、冒頭行のテキストがファイル名になるようになっています。
日本語だったりスペースあったりしますよね…。

そこで日本語でも”/”こういうのはいってても保存できるようにします。
文字列をURIエンコード(エスケープ)・デコードする
これで行ってみます!

encodeURIComponent(removeMarks(filename))

JavaScript | ファイル名に使えない記号を削除
String.prototype.replaceAll()
“/”に関しては↑を参考にしました。

function removeMarks(filename) {
  const marks = ["\\", '/', ':', '*', '?', 'a', "<", ">", '|', /^ */g, /^ */g];
  let filename_removeMarks = filename;
  for (const i in marks) {
    filename_removeMarks = filename_removeMarks.replaceAll(marks[i], '')
    }
  return(filename_removeMarks);
  }

ファイルを読み込めるようにする

次は、ファイルをほかのアプリ(ファイルアプリなど)から読み込めるようにしたいと思います!

prscX/react-native-file-selector
↑が簡単そうなので、使っていきます!

npm install react-native-file-selector --save
import RNFileSelector from 'react-native-file-selector';

使えないので、Undefined is not an object RNFileSelector.Show を見てみると、linkする必要がありそうです。

react-native: command not foundとでてlinkできない!
“react-native link” は何をするか

公式ドキュメントより、npx react-native link react-native-file-selectorしてみます。

warn Calling react-native link [packageName] is deprecated in favor of autolinking. It will be removed in the next major release.
Autolinking documentation: https://github.com/react-native-community/cli/blob/master/docs/autolinking.md

このリンク先によると、linkは非推奨らしい…

Expoでreact-native linkが認識されない
↑あ、察し…
npm uninstall react-native-file-selectorしました。

Expo公式ドキュメントにあったので、こちらを使います。

expo install expo-document-picker
import * as DocumentPicker from 'expo-document-picker';

export async function fileSelect(){
  const data = await DocumentPicker.getDocumentAsync()
  console.log(data);
  }

picture 7

おお!使えました(*´ω`)

picture 8

データはこんな感じで取り出せますね✨
やった~!

読みこんだファイルを保存する

読み込んだファイルは、一時ファイルになるみたいなので、アプリ内に保存できるようにします!
開いた瞬間に保存したいと思います。

async function fileSelect() {
  const filedata = await FS.fileSelect()
  const filecontent = filedata.filecontent
  const filename = filedata.filename

  setDataKey(null)
  setTextInput('')
  setFileName('')

  const key = 'SimpleMD' + data.length + 1

  saveFileData()
}

saveFileData()で保存しています。
むむむ。
↑だとsetStateの値がすぐに更新されないですね…

なぜセットした値が更新されていないのか?
なぜセットした値が更新されていないのかというと、setStateで値が更新されるのは関数が呼び出された後だからです。

【React】「useStateの値を更新しても反映されない!」の解決方法

↑を参考に改善していきます。

async function fileSelect() {
  const filedata = await FS.fileSelect()
  const filename = filedata.filename
  const filecontent = filedata.filecontent
  const key = 'SimpleMD' + (data.length + 1)

  setFileData(key, filename, filecontent)
  saveFileData(filecontent, key, filename)
}

↑変数として持たせるようにしました。

ちょっとこの機能は難しそうなので、後回しにします!

削除機能の作成

データが増えると大変なので、削除機能を作成します。
(ファイルシステム2つあるから移行しないといけないかも…)

storage.remove();
↑これで実装していきます。

export async function removeData(fileData){
  storage.remove(fileData)
    .then(e => {
      console.log("removeData >>" + e);
    }).catch(err => {
      console.log(fileData);
      console.error("removeData >>" + err);
    })
}

なぜか全消しができるようになりました…w

<Text style={styles.filelistText} onPress={props.removeData({key:key})>

関数を渡すときの記述が間違ってました。

削除機能完成(*´ω`)

バックアップ機能をつける

これもシェア機能でディレクトリごとシェアしたらいけそうな気がします。

アプリ内のデータは、manifest.jsonとして保存されているので、これをそのままどこかに保存する形でもいいですね。
(この方法だとバックアップから復元もできるな…)

でも今回は、ファイルにエクスポートしたものをディレクトリごとバックアップできるようにします。


ファイルの時と同じ方法ではできないですねw
どうしよう…

dataList.map(async e =>{
  const name = e.name
  const content = e.text
  const fileUri = cacheDirectoryUri + encodeURIComponent(removeMarks(name))

  await FileSystem.makeDirectoryAsync(cacheDirectoryUri, { intermediates: true })
    .then(e => {
      console.log("makeDirectoryAsync" + e);
    }).catch(err => {
      console.error(err);
    })

  await FileSystem.writeAsStringAsync(fileUri, content, { encoding: FileSystem.EncodingType.UTF8 })
    .then(e => {
      console.log("writeAsStringAsync >>" + e);
    }).catch(err => {
      console.log(fileUri);
      console.error("writeAsStringAsync >>" + err);
    })
Share.share({ url: cacheDirectoryUri })
  .then(e => {
    console.log(Share.sharedAction);
  }).catch(err => {
    console.error(err);
  })
}

ファイルアプリにはシェアできるのですが、ワンドライブとかにシェアできません。
なんでだろう。

この辺はちょっと課題ですね!

今後の予定

ここまでで「テキストエディタが作れるなぁ」と分かってきたので、一旦今作っているのはおいておいて、新しく作りなおそうと思います(*´ω`)

デザインから計画を立てていくので、続きからはその様子をお伝えします~!

↓続き
【8】React Nativeでテキストエディタを作ってみる!【再出発・デザイン編】

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です