対象はAndroidとiOS。
必要なライブラリ
iOSではPhotos.framework
を使う。出力されたXcodeプロジェクトのGeneralタブのLinked Frameworks and Libraries
に足す。
iOS 7以前もケアするならAssetsLibrary.framework
も必要だが、AppleもiOS 9以降しかサポートしていないし今更要らないだろうと思う。
OnPostProcessBuildを使って設定するようにしておくと楽。例えばこんな感じ。
var pbxProject = new PBXProject();
var targetGUID = pbxProject.TargetGuidByName(PBXProject.GetUnityTargetName());
pbxProject.AddFrameworkToProject(targetGUID, "Photos.framework", false);
必要な権限
iOS
PBXProject
のNSPhotoLibraryAddUsageDescription
に何か書く。
出力されたXcodeプロジェクトのInfoタブのCustom iOS Target Properties
に足す。
プロジェクト上の表示名はPrivacy - Photo Library Additions Usage Description
になっている。
ただの説明書きだが、無いとアプリが強制終了する。
あとこの説明は確認ダイアログに表示される(つまりユーザーの目に触れる)。
これもOnPostProcessBuildを使って設定するようにしておくと楽。
PlistDocument plist = new PlistDocument();
plist.ReadFromString(File.ReadAllText(plistPath));
PlistElementDict rootDict = plist.root;
rootDict.SetString("NSPhotoLibraryUsageDescription", "ほげふがぴよぴよ");
File.WriteAllText(plistPath, plist.WriteToString());
Android
WRITE_EXTERNAL_STORAGE
。
Assets/Plugins/Android/AndroidManifest.xml
に<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
を書き足す。
ユーザーへの権限問合せ
保存タイミングではなく事前に権限を確認する。
iOS
iOSでは初回に一度だけ下記2つのボタンがある権限確認ダイアログが表示される。
- 許可(OK)
- 拒否(許可しない)
拒否した場合は、有効にするには設定アプリから許可してもらうしかない。
「設定」アプリ→プライバシー→ストレージ→対象アプリ名 をONにする。
[PHPhotoLibrary authorizationStatus]
で状況を確認し、未確認の場合は[PHPhotoLibrary requestAuthorization:]
で問い合わせる(ダイアログが表示される)。
#import <Photos/Photos.h>
@interface PhotoExtention:NSObject
+ (bool)requestPermission;
@end
@implementation PhotoExtention
+ (BOOL)requestPermission {
PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
if (status == PHAuthorizationStatusAuthorized) {
return YES;
} else if (status == PHAuthorizationStatusNotDetermined) {
__block BOOL isAuthorized = NO;
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
isAuthorized = (status == PHAuthorizationStatusAuthorized);
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
return isAuthorized;
} else {
return NO;
}
}
extern "C" bool _PhotoExtention_RequestPermission() {
return [PhotoExtention requestPermission];
}
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern bool _PhotoExtention_RequestPermission();
if (_PhotoExtention_RequestPermission())
{
// 保存処理
}
else
{
// 拒否された時の処理
}
Android
許可するか永続拒否するまでは何度でも確認ダイアログが表示される。
1回目は「許可」/「許可しない」ボタン。
2回目以降「許可」/「許可しない」ボタン+「今後表示しない」チェックボックス。
なのでパターンとしては以下の通り。
- 許可
- 一時拒否(許可しない)
- 永続拒否(許可しない+今後表示しない)
基本的に一時拒否になるので、再度アクセスしてもらって許可してもらう事ができる。
ちなみに永続拒否した場合、「設定」アプリ→アプリと通知→対象アプリ名 →権限→ストレージをONにする(おそらくベンダーやOSによって場所が異なる)。
多くのアプリでは、アプリ起動時にAndroidManifest.xml
に記載しておいて、起動時にすべて許可しないとアプリを終了といった設計になっている事が多いが、稀に権限付与のタイミングを遅らせたい時がある。
その場合はAndroidのAPIでrequestPermissions
する。
戻りはUnityPlayerActivity.onRequestPermissionsResult
で受け取って、UnityPlayer.UnitySendMessage
でUnity側に戻す。
という諸々を対応してくれるUnityプラグインがあるので、基本的にはそれを使う。
https://github.com/sanukin39/UniAndroidPermission
保存処理
iOS
フォトライブラリーに保存する専用メソッドが準備されているので、それを使う。
ネイティブメソッドの設計上、即座に戻ってこないので、コールバック先のオブジェクトを確保しておく必要がある。
[PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:]
と[[PHPhotoLibrary sharedPhotoLibrary] performChanges:]
を利用して「写真」に保存し、戻りをUnitySendMessage
でどこかにコールバックする。
#import <Photos/Photos.h>
@interface PhotoExtention:NSObject
+ (void)saveImage:(NSString *)path albumName:(NSString *)album;
@end
@implementation PhotoExtention
+ (void)saveImage:(NSString *)path albumName:(NSString *)album {
PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init];
fetchOptions.predicate = [NSPredicate predicateWithFormat:@"localizedTitle = %@", album];
PHFetchResult *fetchResult = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAny options:fetchOptions];
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
PHAssetChangeRequest *assetChangeRequest;
assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:[NSURL fileURLWithPath:path]];
PHAssetCollectionChangeRequest *assetCollectionChangeRequest = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:fetchResult.firstObject];
[assetCollectionChangeRequest addAssets:@[[assetChangeRequest placeholderForCreatedAsset]]];
} completionHandler:^(BOOL success, NSError *error) {
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
if (success)
UnitySendMessage("HogeObject", "OnSaveImageCompleted", "");
else {
UnitySendMessage("HogeObject", "OnSaveImageFailed", [self getCString:[error localizedDescription]]);
}
}];
}
@end
extern "C" void _PhotoExtention_WriteImageToAlbum(const char* path, const char* album) {
[PhotoExtention saveImage:[NSString stringWithUTF8String:path] albumName:[NSString stringWithUTF8String:album]];
}
// 呼び出し側
using System.Runtime.InteropServices;
[DllImport( "__Internal" )]
private static extern void _PhotoExtention_WriteImageToAlbum(string path, string album);
_NativeGallery_ImageWriteToAlbum(path, album);
// 戻り側
public class HogeObject : MonoBehaviour
{
public void OnSaveImageCompleted(string message)
{
if (callback != null)
{
callback(null);
}
}
public void OnSaveImageFailed(string error)
{
if (callback != null)
{
callback(error);
}
}
}
Android
保存するパスをDIRECTORY_DCIM
から取得し、そこに適当なファイル名で書き出す。
public class PhotoExtension
{
public static String getDcimPath()
{
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath();
}
}
var fileFrom = "/path/to/save";
var photoExtention = new AndroidJavaClass("com.example.PhotoExtension");
var saveDir = photoExtention.CallStatic<string>("getDcimPath");
var fileTo = Path.Combine(saveDir, "ファイル名");
File.Copy(fileFrom, fileTo, true);
保存したファイルを見えるようにするため、scanFile
してインデックス情報に登録する必要がある。
public class PhotoExtension
{
public static String scanFile(String fileName)
{
Context context = UnityPlayer.currentActivity.getApplicationContext();
MediaScannerConnection.scanFile(context, new String[] { path }, null, null);
}
}
var photoExtention = new AndroidJavaClass("com.example.PhotoExtension");
photoExtention.CallStatic<string>("scanFile", fileTo);