Unity で作ってるスマホゲームアプリで、スマホ内の画像を使いたいシチュエーションがあったので、画像を選択できるネイティブプラグインを作ってみた。 はまったところを中心に、メモを残しておく。

概要

  • Android は Fragment から Intent を投げて、画像の選択結果を受け取る
  • iOS は UIImagePickerController を開いて、画像の選択結果を受け取る
  • 画像を選択できたら、画像のファイルパスを C# 側にコールバックする
    • オリジナルのファイルパスは取得できないので、自分で読み書きできる場所に画像をコピーして、そのパスを渡す

成果物

unimgpicker_ios

thedoritos/unimgpicker: Image picker for Unity iOS/Android にある。

以下のソースは、説明のため、部分的に示したりエラー処理を省略したりしているので、詳しくはリポジトリの方を参照する。

Android 版

Android 版では、画像ファイルを開く Intent を投げて、外部アプリで画像を開いてもらい、結果を受け取る方針にする。

Fragment でコールバックを受け取る

Intent は startActivityForResult メソッドで投げて、結果を onActivityResult メソッドで受け取る。 各メソッドは Activity にも Fragment にもあり、どちらから投げるかによってどちらにコールバックされるかが決まる。

別の話として、Unity のネイティブプラグインを作るときに、UnityPlayerActivity の継承問題がある。 各々のプラグインが UnityPlayerActivity を継承する設計になってしまっているために、ソースが競合したり、マージが必要になったりしてつらいことがある。

そこで、本プラグインでは、Fragment を使うことで既存の UnityPlayerActivity には手を入れないようにしている。

画像ファイルをコピーする

画像が選択できると onActivityResult が呼ばれる。画像の URI は引数の Intent から簡単に取得することができる。

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    Uri uri = data.getData();
}

ただし、この URI は content:// という特殊な形式になっている。 これは FileProvider | Android Developers という仕組みで、実際のファイルパスを隠蔽しながらファイルを共有しているため。

:bulb: FileProvider でファイルを共有する側は、以前 Unity Android で FileProvider を使用する でやった。

この content:// という URI をそのまま C# 側に渡しても、ファイルを読みに行くことができない。 そこで、Android プラグイン側でファイルを読み、C# 側から読める場所にコピーして、そのパスを返してあげるようにしている。

InputStream inputStream = context.getContentResolver().openInputStream(uri);
FileOutputStream outputStream = context.openFileOutput(mOutputFileName, Context.MODE_PRIVATE);

byte[] buf = new byte[8192];
while (true) {
    int r = inputStream.read(buf);
    if (r == -1) {
        break;
    }
    outputStream.write(buf, 0, r);
}

iOS 版

iOS 版では、UIImagePickerController を使う方針にする。

NSPhotoLibraryUsageDescription

Info.plistNSPhotoLibraryUsageDescription を定義していないと、クラッシュするロックな仕様なので気をつける。

:bulb: 本プラグインでは PostProcessBuild で自動で設定するようにしている。

画像ファイルをコピーする

UIImagePickerControllerDelegate は、メモリに読み込まれた画像を受け取るようになっていて、こちらも実際のファイルパスは隠蔽されている。 ということで、画像を C# 側から読める場所に書き込んで、そのパスを返してあげるようにしている。

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info {
    UIImage *image = info[UIImagePickerControllerOriginalImage];

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

    // 拡張子がないとだめだから
    NSString *imageName = self.outputFileName;
    if ([imageName hasSuffix:@".png"] == NO) {
        imageName = [imageName stringByAppendingString:@".png"];
    }

    NSString *imageSavePath = [(NSString *)[paths objectAtIndex:0] stringByAppendingPathComponent:imageName];

    NSData *png = UIImagePNGRepresentation(image);

    BOOL success = [png writeToFile:imageSavePath atomically:YES];
}

:warning: 上記の例はがっつり省略しているので、詳しくはリポジトリの方を参照する。

Unity で画像を読み込む

プラグインから受け取ったファイルパスに file:// をつけて URL を作って、UnityEngine.WWW で読みにいけば良い。

まとめ・感想

  • Android は Fragment から Intent を投げて、画像の選択結果を受け取った
    • Fragment を使うことで UnityPlayerActivity 継承のコンフリクトを避けることができた
  • iOS は UIImagePickerController を開いて、画像の選択結果を受け取った
    • NSPhotoLibraryUsageDescription の設定が必要がだったので、スクリプトで自動化した
  • 画像を選択できたら、画像のファイルパスを C# 側にコールバックした
    • オリジナルのファイルパスは取得できないので、自分で読み書きできる場所に画像をコピーして、そのパスを渡した

ひさびさに Objective-C 書いたらつらかった :angel:

特に I/O 周りだと nil チェックが必要なことが多くて、Swift では if let とか guard で綺麗に書けるところが if (hoge == nil) の早期リターンだらけになってつらかった。 あと、ググるときにわざわざ「Objective-C」を指定しないとドキュメントが見つけられないのが、タイプ数的にも精神的にもつらかった。

プラグインを Swift で書くのは Unity iOS のネイティブプラグインを Swift で書いた でやっているので、今回も Swift にしても良かったかもしれない。 というか早く iOS ネイティブプラグインも Swift で書くのがデファクトになってほしい :pray:

参考

補足

Texture をリサイズしたい場合に使えそうな情報。