15
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Unityで画像をアルバムに保存してみた

Posted at

対象はAndroidとiOS。

必要なライブラリ

iOSではPhotos.frameworkを使う。出力されたXcodeプロジェクトのGeneralタブのLinked Frameworks and Librariesに足す。
スクリーンショット 2018-06-02 12.42.37.png

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

PBXProjectNSPhotoLibraryAddUsageDescriptionに何か書く。
出力されたXcodeプロジェクトのInfoタブのCustom iOS Target Propertiesに足す。
プロジェクト上の表示名はPrivacy - Photo Library Additions Usage Descriptionになっている。
スクリーンショット 2018-06-02 12.53.18.png
ただの説明書きだが、無いとアプリが強制終了する。
あとこの説明は確認ダイアログに表示される(つまりユーザーの目に触れる)。

これも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);
15
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?