ALAssetsLibraryについて

 以下は私がWebや書籍から集めた情報や、私が試行錯誤した経験により、ALAssetsLibraryについてまとめたものです。
そのため、間違いが含まれている可能性があります。何かしらの間違いを見つけた方はご指摘願います。

はじめに

 ALAssetsLibraryとはiOS4で追加された、iPhone/iPad/iPodの写真/映像フォルダにアクセスするためのフレームワークです。
 私はiOS4がリリースされた頃からiOSプログラミングを始めたため、iOS3以前については良く知りませんが、iOS3以前ではファイルパスを指定し、写真/映像フォルダに直接アクセスしていたようです。しかし現在はファイルに直接アクセスするとAppleの審査が通らないようです。そのため現在iPhone/iPad/iPodの写真・映像フォルダにアクセスするためにはALAssetsLibraryを使用しないといけません。
 ALAssetsLibraryを使用すると画像データのEXIFデータなども取得でき、便利です。
 ALAssetsLibraryがリリースされてから半年程度経ちますが、ALAssetsLibraryに関するサンプルコードやチュートリアルなどはかなり少ないです。そのため今回ALAssetsLibraryについてまとめることにしました。

サンプルコード

MYAssetsAccessor.h

#import <UIKit/UIKit.h>


@class ALAssetsLibrary;


@interface MYAssetsAccessor : NSObject {
 @private
  ALAssetsLibrary *assetsLibrary_;
}


- (void)loadUrlsWithCallbackTarget:(id)anObject
                    callbackMethod:(SEL)aSelector;
- (NSDictionary *)metadataOfAssetIdentifiedByUrl:(NSURL *)url;
- (UIImage *)thumbnailOfAssetsIdentifiedByUrl:(NSURL *)url;
- (UIImage *)fullResolutionImageOfAssetsIdentifiedByUrl:(NSURL *)url;


+ (void)addObserverForAssetsLibraryChange:(id)anObserver
                                 selector:(SEL)aSelector;
+ (void)removeObserverForAssetsLibraryChange:(id)anObserver;


// to understand how the enumeration works.
- (void)enumerationTest_;

@end


MYAssetsAccessor.m

#import <AssetsLibrary/AssetsLibrary.h>  // require AssetsLibrary.framework
#import "MYAssetsAccessor.h"


@implementation MYAssetsAccessor

- (id)init {
  assetsLibrary_ = [[ALAssetsLibrary alloc] init];
  return self;
}


- (void)dealloc {
  [assetsLibrary_ release];
  [super dealloc];
}


- (void)loadUrlsWithCallbackTarget:(id)anObject
                    callbackMethod:(SEL)aSelector {
  id callbackTarget = [anObject retain];
  SEL callbackMethod = aSelector;
  
  NSMutableArray *urls = [[NSMutableArray alloc] init];
  
  void (^assetResultBlock)(ALAsset *, NSUInteger, BOOL *) =
  ^(ALAsset *asset, NSUInteger index, BOOL *stop) {
    if (!*stop && asset) {
      ALAssetRepresentation *defaultRepresentation = [asset defaultRepresentation];
      [urls addObject:[defaultRepresentation url]];
    }
  };
	
  void (^resultBlock)(ALAssetsGroup *, BOOL *) =
  ^(ALAssetsGroup *group, BOOL *stop) {
    if (*stop || !group) {
      [callbackTarget performSelector:callbackMethod withObject:urls];
    } else {
      [group setAssetsFilter:[ALAssetsFilter allAssets]];
      [group enumerateAssetsUsingBlock:assetResultBlock];
    }
  };
  
  void (^failureBlock)(NSError *) =
  ^(NSError *error) {
    NSLog(@"exception in enumerating assets. %@", error);
    [callbackTarget performSelector:callbackMethod withObject:urls];
  };
  
  ALAssetsGroupType targetGroupType = ALAssetsGroupAll;
  [assetsLibrary_ enumerateGroupsWithTypes:targetGroupType
                                usingBlock:resultBlock
                              failureBlock:failureBlock];
  
  [urls release];
  [callbackTarget release];
}


- (NSDictionary *)metadataOfAssetIdentifiedByUrl:(NSURL *)url {
  __block NSDictionary *metadata = nil;
  [assetsLibrary_ assetForURL:url
                  resultBlock:^(ALAsset *asset) {
                    ALAssetRepresentation *defaultRepresentation = [asset defaultRepresentation];
                    metadata = [[defaultRepresentation metadata] copy];
                  }
                 failureBlock:^(NSError *error) {
                   NSLog(@"exception in accessing assets by url. %@", error);                      
                 }];
  return [metadata autorelease];
}


- (UIImage *)thumbnailOfAssetsIdentifiedByUrl:(NSURL *)url {
  __block UIImage *thumbnail = nil;
  [assetsLibrary_ assetForURL:url
                  resultBlock:^(ALAsset *asset) {
                    thumbnail = [[UIImage alloc] initWithCGImage:[asset thumbnail]];
                  }
                 failureBlock:^(NSError *error) {
                   NSLog(@"exception in accessing assets by url. %@", error);                      
                 }];
  return [thumbnail autorelease];
}


- (UIImage *)fullResolutionImageOfAssetsIdentifiedByUrl:(NSURL *)url {
  __block UIImage *fullResolutionImage = nil;
  [assetsLibrary_ assetForURL:url
                  resultBlock:^(ALAsset *asset) {
                    ALAssetRepresentation *defaultRepresentation = [asset defaultRepresentation];
                    fullResolutionImage = [[UIImage alloc] initWithCGImage:[defaultRepresentation fullResolutionImage]];
                  }
                 failureBlock:^(NSError *error) {
                   NSLog(@"exception in accessing assets by url. %@", error);                      
                 }];
  return [fullResolutionImage autorelease];
}


+ (void)addObserverForAssetsLibraryChange:(id)anObserver
                                 selector:(SEL)aSelector {
  NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
  [defaultCenter addObserver:anObserver
                    selector:aSelector
                        name:ALAssetsLibraryChangedNotification
                      object:nil];
}


+ (void)removeObserverForAssetsLibraryChange:(id)anObserver {
  NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
  [defaultCenter removeObserver:anObserver
                           name:ALAssetsLibraryChangedNotification
                         object:nil];
}


// to understand how the enumeration works.
- (void)enumerationTest_ {
  void (^assetResultBlock)(ALAsset *, NSUInteger, BOOL *) =
  ^(ALAsset *asset, NSUInteger index, BOOL *stop) {
    if (*stop) {
      NSLog(@"stopped enumerating assets.");
    } else if (!asset) {
      NSLog(@"the end of the group.");
    } else {
      NSDictionary *urls = [asset valueForProperty:ALAssetPropertyURLs];
      NSLog(@"%@", urls);
    }
  };
	
  void (^resultBlock)(ALAssetsGroup *, BOOL *) =
  ^(ALAssetsGroup *group, BOOL *stop) {
    if (*stop) {
      NSLog(@"stopped enumerating groups.");
    } else if (!group) {
      NSLog(@"the end of enumation.");
    } else {
      NSString *groupName = [group valueForProperty:ALAssetsGroupPropertyName];
      
      ALAssetsFilter *photosFilter = [ALAssetsFilter allPhotos];
      [group setAssetsFilter:photosFilter];
      NSInteger numberOfPhotos = [group numberOfAssets];
      NSLog(@"%d photos in %@", numberOfPhotos, groupName);
      [group enumerateAssetsUsingBlock:assetResultBlock];
      
      ALAssetsFilter *videosFilter = [ALAssetsFilter allVideos];
      [group setAssetsFilter:videosFilter];
      NSInteger numberOfVideos = [group numberOfAssets];
      NSLog(@"%d videos in %@", numberOfVideos, groupName);
      [group enumerateAssetsUsingBlock:assetResultBlock];
      
      ALAssetsFilter *allFilter = [ALAssetsFilter allAssets];
      [group setAssetsFilter:allFilter];
      NSInteger numberOfAssets = [group numberOfAssets];
      NSLog(@"%d assets in %@", numberOfAssets, groupName);
      [group enumerateAssetsUsingBlock:assetResultBlock];
    }
  };
  
  void (^failureBlock)(NSError *) =
  ^(NSError *error) {
    NSLog(@"exception in enumerating assets. %@", error);
  };
	
  // ALAssetsGroupType targetGroupType = ALAssetsGroupLibrary |
  //   ALAssetsGroupAlbum |
  //   ALAssetsGroupEvent |
  //   ALAssetsGroupFaces |
  //   ALAssetsGroupSavedPhotos;
  ALAssetsGroupType targetGroupType = ALAssetsGroupAll;
  [assetsLibrary_ enumerateGroupsWithTypes:targetGroupType
                                usingBlock:resultBlock
                              failureBlock:failureBlock];
  
  // the result will be:
  // 2011-04-28 18:38:40.939 MyProject[869:707] 4 photos in Photo Library
  // 2011-04-28 18:38:40.964 MyProject[869:707] {
  //   "public.jpeg" = "assets-library://asset/asset.jpg?id=104&ext=jpg";
  // }
  // ...
  // 2011-04-28 18:38:40.989 MyProject[869:707] the end of the group.
  // 2011-04-28 18:38:40.993 MyProject[869:707] 0 videos in Photo Library
  // 2011-04-28 18:38:40.997 MyProject[869:707] the end of the group.
  // 2011-04-28 18:38:41.003 MyProject[869:707] 4 assets in Photo Library
  // 2011-04-28 18:38:41.008 MyProject[869:707] {
  //   "public.jpeg" = "assets-library://asset/asset.jpg?id=104&ext=jpg";
  // }
  // ...
  // 2011-04-28 18:38:41.030 MyProject[869:707] the end of the group.
  // 2011-04-28 18:38:41.039 MyProject[869:707] 25 photos in Camera Roll
  // 2011-04-28 18:38:41.053 MyProject[869:707] {
  //   "public.jpeg" = "assets-library://asset/asset.JPG?id=1000000021&ext=JPG";
  // }
  // ...
  // 2011-04-28 18:38:41.273 MyProject[869:707] the end of the group.
  // 2011-04-28 18:38:41.277 MyProject[869:707] 4 videos in Camera Roll
  // 2011-04-28 18:38:41.285 MyProject[869:707] {
  //   "com.apple.quicktime-movie" = "assets-library://asset/asset.MOV?id=1000000004&ext=MOV";
  // }
  // ...
  // 2011-04-28 18:38:41.314 MyProject[869:707] the end of the group.
  // 2011-04-28 18:38:41.319 MyProject[869:707] 29 assets in Camera Roll
  // 2011-04-28 18:38:41.327 MyProject[869:707] {
  //   "public.jpeg" = "assets-library://asset/asset.JPG?id=1000000021&ext=JPG";
  // }
  // ...
  // 2011-04-28 18:38:41.569 MyProject[869:707] the end of the group.
  // 2011-04-28 18:38:41.573 MyProject[869:707] the end of enumation.
}

@end


AppDelegate_iPhone.h

#import <UIKit/UIKit.h>


@class MYAssetsAccessor;


@interface AppDelegate_iPhone : NSObject <UIApplicationDelegate> {
  UIWindow *window_;
  MYAssetsAccessor *myAssetsAccessor_;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;

@end


AppDelegate_iPhone.m

#import "AppDelegate_iPhone.h"
#import "MYAssetsAccessor.h"


@interface AppDelegate_iPhone(Private)

- (void)assetLibraryDidChange_:(NSNotification *)aNotification;
- (void)assetUrlsDidLoad_:(NSArray *)urls;

@end


@implementation AppDelegate_iPhone
@synthesize window = window_;

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
  [self.window makeKeyAndVisible];
  myAssetsAccessor_ = [[MYAssetsAccessor alloc] init];
  [MYAssetsAccessor addObserverForAssetsLibraryChange:self
                                             selector:@selector(assetLibraryDidChange_:)];
  [myAssetsAccessor_ enumerationTest_];
  [myAssetsAccessor_ loadUrlsWithCallbackTarget:self
                                 callbackMethod:@selector(assetUrlsDidLoad_:)];
  return YES;
}


- (void)applicationWillTerminate:(UIApplication *)application {
  [MYAssetsAccessor removeObserverForAssetsLibraryChange:self];
}


- (void)dealloc {
  [myAssetsAccessor_ release];
  [window_ release];
  [super dealloc];
}

@end


@implementation AppDelegate_iPhone(Private)

- (void)assetLibraryDidChange_:(NSNotification *)aNotification {
  NSLog(@"asset library changed. %@", aNotification);
  // TODO reload urls
}


- (void)assetUrlsDidLoad_:(NSArray *)urls {
  NSLog(@"%d urls, %@", [urls count], urls);
  if ([urls count] == 0) {
    return;
  }
  NSURL *url = [urls objectAtIndex:30];
  NSDictionary *metadata = [myAssetsAccessor_ metadataOfAssetIdentifiedByUrl:url];
  NSLog(@"metadata of %@ is %@", url, metadata);
  UIImage *thumbnail = [myAssetsAccessor_ thumbnailOfAssetsIdentifiedByUrl:url];
  NSLog(@"thumbnail of %@ is %@", url, thumbnail);
  UIImage *fullResolutionImage = [myAssetsAccessor_ fullResolutionImageOfAssetsIdentifiedByUrl:url];
  NSLog(@"full resolution image of %@ is %@", url, fullResolutionImage);
}

@end

サンプルコード概要

 MYAssetsAccessorはALAssetsLibraryを利用した、画像/映像フォルダへアクセスするためのクラスです。AppDelegate_iPhoneはMYAssetsAccessorを利用したコードです。

 MYAssetsAccessorのloadUrlsWithCallbackTarget:callbackMethod:で画像/映像フォルダ中の画像/映像を表すURLの配列を取得します。メソッド中のenumerateGroupsWithTypes:usingBlock:failureBlock:は非同期で実行されるため、URLの取得が終了するもしくは何らかのエラーが生じた場合、指定したcallbackメソッドを呼び出します。なお、ブロック内で*stop = YES;とすると、走査を終了させることもできます。
 URLの一覧が取得できれば後はメタデータや、サムネイルを取得したりといろいろできます。

 URLは画像/映像フォルダに何らかの変更が生じた場合、変更される事があるので、ALAssetsLibraryChangeNotificationを監視しておきます。

 MYAssetsAccessorには動作確認用のenumerationTest_メソッドを用意しています。画像/映像フォルダを走査して、URL等をログに書き出します。


サンプルコードはMITライセンスです。
 MYAssetsAccessor.h 直
 MYAssetsAccessor.m 直
 AppDelegate_iPhone.h 直
 AppDelegate_iPhone.m 直

注意点

ALAssetsLibraryのインスタンスのメソッドはメインスレッドで呼ぶ方が良い

 ALAssetsLibraryのインスタンスのメソッドを初めて呼び出すと、次のようなアラートが表示されます。

 メインスレッドで呼び出した場合は上記のアラートが表示されます。
 しかし、メインスレッド以外で呼び出した場合は上記のアラートが表示されず、ユーザの入力待ち状態のままになり、ALAssetsLibraryに関する処理が一向に進まなくなります。
 なお、上記のアラートでユーザがDon't Allowを選択すると写真フォルダにアクセスできず、failureBlockが呼び出されます。

ALAssetsLibraryのインスタンスのメモリ消費量は写真/映像フォルダ内のコンテンツ量に応じて増える

 画像フォルダに10枚しか写真がない場合と、画像フォルダに10,000枚の写真がある場合とでは、ALAssetsLibraryのインスタンスのメモリ消費量がかなり違います。検索用インデックスでも作っているのでしょうか。
 画像/映像フォルダ内のコンテンツ量が増えるにつれ、アプリケーションで使用できるメモリ量が少なくなってしまいます。動作保障コンテンツ数などを決める必要があるのかもしれません。iPhone/iPad/iPodのデフォルトの写真ビューアも画像が80,000枚程度保存されていると強制終了するようになりますし。

ALAssetsLibraryChangeNotificationはバックグラウンド状態では受信できない

 1. 対象のアプリケーションをバックグラウンド状態にする
 2. 他のアプリケーションを起動/フォアグラウンド状態にする
 3. 他のアプリケーションで画像/映像フォルダに変更が生じる(新たな画像を追加する、など)
 4. 他のアプリケーションを終了/バックグラウンド状態にする
 5. 対象のアプリケーションをフォアグラウンド状態にする
 上記のようなアプリケーションの切り替えなどを行った場合、対象のアプリケーションがALAssetsLibraryChangeNotificationを受信するのは5のタイミングです。3の画像/映像フォルダに変更が生じた際ではなく、5の対象のアプリケーションがフォアグラウンド状態になったときです。

iTunesの同期ではALAssetsLibraryChangeNotificationは発生しない

 iTunesの同期により画像/映像フォルダに変更が生じても、ALAssetsLibraryChangeNotificationは発生しません。

iTunesで同期すると画像のEXIFは消失する

 PCからiTunes経由でiPhone/iPad/iPodに画像を転送した場合、転送元のPC上の画像にEXIFが付属していたとしても、iPhone/iPad/iPodに転送される際に削除されるようです。
 下記参考文献のiOS4プログラミングブックの中でも次のように述べられています。

PC上のオリジナル画像がExif情報付きだったとしても、同期するタイミングで情報が削られているようです。

 ただし、iPhone/iPad/iPodのカメラで撮った写真に関しては、iTuneの同期でEXIFは削除されません。おそらくApple独自のメタデータ形式などがあるのでしょう。

iTunesで同期すると画像はJpegとして保存される

 ALAssetsLibraryと直接関係はないですが、iTunesで同期すると、bmppngjpegに変換されてiPhone/iPad/iPod上に保存されるようです。

画像/映像の読み込みと新規書き込みのみ可能

 ALAssetsLibraryでは、画像/映像の読み込みと新規書き込みのみが可能で、削除や修正/上書きなどはできないようです。

参考

公式リファレンス

ALAssetsLibrary Class Reference
 Apple公式のリファレンスです。

読み込み

iOS 4の新機能13選&AssetsLibraryで作る画像ビューア (2/4):SDKで始めるiPad/iPhoneアプリ開発の勘所(終) - @IT
 ALAssetsLibraryを使用した画像ビューアの作成方法。ソースコードのダウンロードもできます。
iPhoneの画像からEXIF,GPSデータを取得する ~ guess what?
 GPSデータの読み込みなど。

書籍

iOS4プログラミングブック

iOS4プログラミングブック

 iOS4で追加された機能に焦点をあてた本。多くはありませんがALAssetsLibraryについての記述があります。
私が知っている限り、ALAssetsLibraryについて書かれている日本語の本はこれだけです。