Unity iOS/Android で端末内の画像を使うためのプラグインを書いた
Unity で作ってるスマホゲームアプリで、スマホ内の画像を使いたいシチュエーションがあったので、画像を選択できるネイティブプラグインを作ってみた。 はまったところを中心に、メモを残しておく。
概要
- Android は
FragmentからIntentを投げて、画像の選択結果を受け取る - iOS は
UIImagePickerControllerを開いて、画像の選択結果を受け取る - 画像を選択できたら、画像のファイルパスを C# 側にコールバックする
- オリジナルのファイルパスは取得できないので、自分で読み書きできる場所に画像をコピーして、そのパスを渡す
成果物

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 という仕組みで、実際のファイルパスを隠蔽しながらファイルを共有しているため。
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.plist に NSPhotoLibraryUsageDescription を定義していないと、クラッシュするロックな仕様なので気をつける。
本プラグインでは 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];
}
上記の例はがっつり省略しているので、詳しくはリポジトリの方を参照する。
Unity で画像を読み込む
プラグインから受け取ったファイルパスに file:// をつけて URL を作って、UnityEngine.WWW で読みにいけば良い。
まとめ・感想
- Android は
FragmentからIntentを投げて、画像の選択結果を受け取った-
Fragmentを使うことでUnityPlayerActivity継承のコンフリクトを避けることができた
-
- iOS は
UIImagePickerControllerを開いて、画像の選択結果を受け取った-
NSPhotoLibraryUsageDescriptionの設定が必要がだったので、スクリプトで自動化した
-
- 画像を選択できたら、画像のファイルパスを C# 側にコールバックした
- オリジナルのファイルパスは取得できないので、自分で読み書きできる場所に画像をコピーして、そのパスを渡した
ひさびさに Objective-C 書いたらつらかった ![]()
特に I/O 周りだと nil チェックが必要なことが多くて、Swift では if let とか guard で綺麗に書けるところが if (hoge == nil) の早期リターンだらけになってつらかった。
あと、ググるときにわざわざ「Objective-C」を指定しないとドキュメントが見つけられないのが、タイプ数的にも精神的にもつらかった。
プラグインを Swift で書くのは Unity iOS のネイティブプラグインを Swift で書いた でやっているので、今回も Swift にしても良かったかもしれない。
というか早く iOS ネイティブプラグインも Swift で書くのがデファクトになってほしい ![]()
参考
- android - onActivityResult is not being called in Fragment - Stack Overflow
- FileProvider | Android Developers
- Storage Access Framework | Android Developers
- [objective c - iOS 10 error [access]
when using UIImagePickerController - Stack Overflow](https://stackoverflow.com/questions/38236723/ios-10-error-access-private-when-using-uiimagepickercontroller)
補足
Texture をリサイズしたい場合に使えそうな情報。