フォトアルバムから写真を取得する

UIImagePicker(Controller)を使用してカメラから撮影した写真、又はフォトアルバムに格納されている写真をアプリケーションに取り込む場合、delegateを自身にセットして以下のようなコードを使うだろう。

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    UIImage* image = [info objectForKey:UIImagePickerControllerOriginalImage];
    〜
}

このコードは写真の取得先がカメラであってもフォトアルバムであっても透過だが、フォトアルバムの場合はストレージのどこかに格納されているファイルのはずであり、その場所が分かればリソースを消費するUIImageよりも都度ストレージから読み込んだ方が効率的だ。

同デリゲートメソッドのinfoパラメタ辞書から以下のようにフォトアルバムの場所をURLとして取得することができる。

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    UIImage* image = [info objectForKey:UIImagePickerControllerOriginalImage];
    NSURL* imageurl = [info objectForKey:UIImagePickerControllerReferenceURL];
}

なのでこのURLを使って都度UIImageを生成すれば良さげだが...

    NSURL* imageurl = [info objectForKey:UIImagePickerControllerReferenceURL];
    UIImage* image = [[UIImage alloc] initWithData:[[NSData alloc] initWithContentsOfURL:imageurl]];

残念ながらこれでは駄目だ。デバッグして同URLの内容をダンプしてみよう

//シミュレータでテストした際のURL
UIImagePickerControllerReferenceURL = "assets-library://asset/asset.JPG?id=142CAFB1-6DE2-4C86-A78E-31962BBF935F&ext=JPG"; 

objectForKey:UIImagePickerControllerReferenceURLで取得できるURLはiOSのアセットを指す特殊なスキーマのURLであり、これを介してフォトアルバムにアクセスするには専用のフレームワークが必要だ。

具体的にはXcodeでプロジェクトにAssetLibrary.Frameworkを追加する必要がある。

その上で同フレームワークの複合ヘッダをインポートしてフォトアルバムにアクセスするコードを書く。

#import <AssetsLibrary/AssetsLibrary.h>-(UIImage*)photoFromALAssets:(NSURL*)imageurl
{
    ALAssetsLibrary* library = [[ALAssetsLibrary alloc] init];
    UIImage* image = nil;
    [library assetForURL:imageurl
              resultBlock: ^(ALAsset *asset)
              {
                   ALAssetRepresentation *representation;
	           representation = [asset defaultRepresentation];
		   image = [[UIImage alloc] initWithCGImage:representation.fullResolutionImage];
              }
              failureBlock:^(NSError *error) 
              {
                  NSLog(@"error:%@", error);
              }];
    return image;
}

ALAssetsLibraryはBlocksに対応しており、取得時とエラー時の処理をブロックで記述する。
これで完璧だと思ったのだが、何度やってもimageがnullで戻ってしまう。

それもそのはずassetForURL:resultBlock:failureBlock:メッセージはブロックが非同期に呼ばれるため、実行の直後でリターンしてもまず取得には失敗する。

なのでコードではリターン前にブロック処理の待ち合わせが必要だが、これも幾つか方法がある。
今回はGCDのセマフォによる待ち合わせを使うことにした。※ 

-(UIImage*)photoFromALAssets:(NSURL*)imageurl
{
	dispatch_semaphore_t sema = dispatch_semaphore_create(0);
	dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
	ALAssetsLibrary* library = [[ALAssetsLibrary alloc] init];
	
	__block UIImage* image = nil;
	//ALAssetsLibrary読み込みの待ち合わせ
	dispatch_async(queue,
	^{
	    [library assetForURL:imageurl
		 resultBlock: ^(ALAsset *asset)
		 {
		     ALAssetRepresentation *representation;
		     representation = [asset defaultRepresentation];
		     image = [[UIImage alloc] initWithCGImage:representation.fullResolutionImage];
		     dispatch_semaphore_signal(sema);
		 }
                 failureBlock:^(NSError *error) {
		     NSLog(@"error:%@", error);
		     dispatch_semaphore_signal(sema);
                 }];
	});
	dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
	return image;
}

これで期待通りに動作した。
ということでiOSの内部アセットへのアクセスは面倒だ。

※大げさなコードを書きたくない場合はNSRunLoopを使っても上手くいくかもしれない。(効率は落ちるだろうが)