【React Native iOS】外部アプリからファイルを開く方法

※注意：この記事はiOSのことだけ書いてます。

# 先に結論

1. Document Typesを設定する
2. [Linking](https://reactnative.dev/docs/linking)を使ってファイルパスを取得する

以上。


# 何がしたいのか
個人でReact Nativeを使ってアプリ開発をしている中で、他のアプリからファイルを受け取る必要があり、今回実装してみました。

正確になんて言うのか、言葉はわからないのですが……これです👇 
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265811/0aabfdea-0a61-3b0a-4cbd-eb6dbd619f7d.png" width="300px" />

共有シートから、特定のアプリでファイルを開くヤツ。
メールやSafari、AirDropで共有されたファイルも開けるようになります。

……なんて言うのが正しいの？


### 実装環境
- React Native 0.63.4
- XCode 12.3

なおRNプロジェクトの作成方法は割愛します。

# 📲 外部アプリからファイルを取得する

## 1. 受け取るファイルの種類を設定する

初っ端からXcodeを開きます。
Projectを開いたら、`Target < [APP名] < Info < Document Types`を開いて、アコーディオン内の`+`ボタンを押してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265811/b25b84a6-c9a1-f167-6d99-205d46cad3eb.png)

ここに、受け取るファイルの種類を定義していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265811/043f3ca5-de70-0cc2-fb11-f1578a943d2f.png)

- Nameは適当に、ファイル形式の種類の名前を入れます。
- Typeには、予め決められているMIMEのようなものを入れます。正しくは `Uniform Type Identifier (UTI)` と言うらしいです。<br/> 必要な値は[ここ](https://developer.apple.com/library/archive/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html)から探せます。その他、独自形式も定義できるみたいです。

- `Handler Rank` は指定した形式のファイルに対して、閲覧者なのか作成者なのかを宣言して、他のアプリとのランク付けをするもの、らしい、です。

| 値 | 意味 |
|---|---|
| Owner | 指定した種類のファイルを作成します |
| Default | 指定した種類のファイルを開きます |
| Alternate | 指定した種類のファイルのセカンダリビューアー | 
| None | 指定した種類のファイルに対して何もしない |


## 2. 受け取ったファイルをRNに送るためのコードを書く

次に、受け取ったファイル（正しくはファイルのパス）を、React NativeのJavaScript側に送るためのコードを書きます。
コードを書くファイルは、`AppDelegate.m`です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265811/93b46f59-75be-8da1-5247-bc4e17580547.png)

追加するコードは、（大分わかりにくいですが……）以下の緑色のコードです。
上の方で`React/RCTLinkingManager.h`をインポートして、あとは下の方で3行だけコードを入れれば、ひとまず完成です。

```diff:AppDelegate.m
#import "AppDelegate.h"

#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
+ #import <React/RCTLinkingManager.h>

~~~中略~~~

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
   ~~中略~~
}

+ - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
+  return [RCTLinkingManager application:application openURL:url options:options];
+ }

```

## 3. JS側でファイルパスを受け取る

残るは、ReactNative側でファイルパスを受け取るだけです。
コードから予想できた方も多いと思いますが、URL Schemeで使う[Linking](https://reactnative.dev/docs/linking)を使ってパスを取得します。
というか2と3に関しては、ドキュメントそのままです。

```tsx:
const handler = (filePath: string) => {
    // 処理内容
};

useEffect(() => {
  // アプリ起動前に呼ばれる
  Linking.getInitialURL().then((filePath) => {
    if (filePath) handler(filePath);
  });

　// アプリ起動後に呼ばれる
  Linking.addEventListener('url', (event) => handler(event.url));
}, []);
```

URL Schemeで取得するURLの多くは、 `https://~~`や `mailto:~~`だと思いますが、ここで取得されるパスは `file:///~~~`になっていて、iOS内部の絶対パスが渡ってきます。

## 4. ファイルパスを使って処理する

ここに関しては、必要に応じて処理をしてください。
私は[react-native-fs](https://github.com/itinance/react-native-fs)を導入してreadFileで読み込みました。


# 🤔 LSSupportsOpeningDocumentsInPlaceと共存させる方法

iOS11から追加されたファイル.appから、各アプリのディレクトリにアクセスできるヤツ。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265811/76cb8c3c-c83d-3d97-d9b2-0e2e850c1a6d.png" width="300px" />

これの有効に必要な`LSSupportsOpeningDocumentsInPlace`をオンにしていると、上記の方法で取得したファイルパスを使っても、`No such file or directory`と怒られてしまう場合があります。

`LSSupportsOpeningDocumentsInPlace`を有効にすると、ファイルをコピーせず直接オリジナルのファイルを参照しにいきます。
iOSはサンドボックスの制限が厳しいので、（正確な理由はわからんですが）おそらくJS側からオリジナルのファイルがあるパスにアクセスする権限がないのが原因かなと。

回避策として、tmpディレクトリにファイルをコピーしてしまう事にしました。

```objective_c:AppDelegate.m

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
  NSString* urlPath = url.path;
  if(![[NSFileManager defaultManager] isReadableFileAtPath:urlPath])
  {
    if([url startAccessingSecurityScopedResource])
    {
      NSString* destPath = [NSString stringWithFormat:@"%@/%@", NSTemporaryDirectory(), [url.path lastPathComponent]];
      if(![[NSFileManager defaultManager] copyItemAtPath:urlPath toPath:destPath error:nil]) {
        [[NSFileManager defaultManager] removeItemAtPath:destPath error:nil];
        [[NSFileManager defaultManager] copyItemAtPath:urlPath toPath:destPath error:nil];
      }
      [url stopAccessingSecurityScopedResource];
      NSURL* destURL = [NSURL fileURLWithPath:destPath];
      return [RCTLinkingManager application:application openURL:destURL options:options];
    }
  }
  return [RCTLinkingManager application:application openURL:url options:options];
}
```
読み取り権限がないファイルパスだった場合のみ、アプリのtempDirにファイルをコピーして、RNにコピーしたファイルのパスを渡すように変更しました。
これで、JS側からもファイルを読みに行くことができました🎉

# 〆
普段Web言語ばかり扱っているので、こうしてネイティブ部分のコードに触れて理解できるようになると楽しくなってきますね。
時間を作って、Obj-CやSwiftもしっかり勉強したいと思いました。

なおネイティブアプリをまともに書いた経験が無いので、間違いなどあればご指摘ください🙇‍♂️