前回は、ファイルの保存先ディレクトリの所得方法、サブディレクトリの作成方法から、実際に画像ファイルを書き出すまでをやったわけです。
 アプリケーションが使用を許されるディレクトリの一つ、Documentディレクトリ(以後{Sandbox}/Document/と記述)のフルパス文字列を手に入れて、直下にimagesディレクトリを作成、その下に色違いのハート画像(1000x1000)のPNGファイルを10個作ったわけですよ。

 去年の事ですがねぇえええ。
 あんまり間があいたんで、何やったかすっかり忘れてたわ、自分。

 ま、その間があいた原因のドリル本では、Q16、17やQ19として扱ってる内容っす。
 で、今回は、この書き出されたPNGファイルを一覧として表示しようちゅーわけですよ。
 これはドリル本では、Q22、24で扱ってます。

 まず、{Sandbox}/Document/imagesディレクトリ内に存在するファイルやディレクトリ名(今回はPNGファイルのみしか存在しない)の一覧を取るにはNSFileManagerの
-contentsOfDirectoryAtPath:error:

 を使います。
 こいつに、前回の要領で{Sandbox}/Document/imagesディレクトリパス文字列を作って渡してやると戻されるNSArrayには、指定したディレクトリ内に存在するファイルやディレクトリの名前が入っているわけですな。NSArrayに配列として収納されているオブジェクトはNSStringです。
 あとerror:側の引数に
NSError* error;

 のポインタを渡してやる事で、なにかエラーが発生した場合に、その詳細を調べる事ができます。使い方はこんな感じ
// imagesディレクトリパス文字列を返す
- (NSString*)imageDirectory
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString* imageDirectory = [documentsDirectory
stringByAppendingPathComponent:@"images"];
return imageDirectory;
}
・・・
NSString* imageDirectory = [self imageDirectory]; //imagesディレクトリパス文字列を返す
NSError* error = nil;
NSArray* images = [[NSFileManager defaultManager]
contentsOfDirectoryAtPath:imageDirectory error:&error];
if (error) {
NSLog(@"%@", error);
return nil;
}
・・・

 ただし、この戻されたNSArrayは名前の配列(test0.png、test1.png、…)であってフルパスの配列ではないわけです。
 なので前回imagesディレクトリのフルパスを作った要領でフルパス文字列にして、それを別に作ったNSMutableArrayに-addObject:していきます。最終的にはこんな感じ。
- (NSArray*)images
{
NSString* imageDirectory = [self imageDirectory];
NSError* error = nil;
NSArray* names = [[NSFileManager defaultManager]
contentsOfDirectoryAtPath:imageDirectory error:&error];
if (error) {
NSLog(@"%@", error);
return nil;
}
NSMutableArray* images = [NSMutableArray arrayWithCapacity:[names count]];
for (NSString* name in names) {
[images addObject:[imageDirectory stringByAppendingPathComponent:name]];
}
return images;
}

 +arrayWithCapacity:ってのはNSMutableArrayの簡易コンストラクタなんだけど、引数でどのくらいの大きさの配列を作るつもりなのかをあらかじめ伝える事ができます。
 こうする事で、思ってたよりオブジェクトの格納用メモリが必要になって再確保とかいうロスが防げます。ま、いうほどロスではないので、+arrayを使ってもいいと思うけど。for inループは
for (int i = 0; i < [names count]; i++) {
NSString* name = [names objectAtIndex:i];
・・・

 と動作結果は同じです。ただし、こっちの方が若干速い。

 こいつをThumbnailViewViewControllerに渡して、各画像ファイルを読み込ませ、画像をタイル表示させるわけですよ。
 渡すのはプロパティ経由。プロパティ?な人はドリル本の付録ブートキャンプの「Objective-Cの基礎知識」を読みましょう。ちと古いが「iPhoneアプリ開発、号外 propertyをちょっと調べてみた」でも可。
@interface ThumbnailViewViewController : UIViewController
@property (copy) NSArray* images;
@end

 属性をretainじゃなくcopyにする理由は、受け取ったThumbnailViewViewControllerと渡した側でNSArrayインスタンスを共有させたくないから。共有させると渡した側で並びを変えたりした時にThumbnailViewViewController側も影響受けるからです。
 NSArrayが収納しているオブジェクトまで複製されるわけではないんで、そんなにロスはありません。浅いコピー(shallow copy)とか呼ばれてます。

$テン*シー*シー-12

 どうでもいいけどThumbnailViewViewControllerってViewを2回使っとるがな、今気づいた。
 ああ~、View based Applicationテンプレート使って、プロジェクト名ThumbnailViewにしたからだね~。自動的に「プロジェクト名+ViewController」になっちゃうんだよ。

 へんてこ名が気になる人は、手作業でファイル名を変更したりせずにEdit→Refactor→Rename…メニュー使いましょう。

 1)変更したいクラス名を選択(文字カーソルをクラス名のどこかに置くだけでも可)して変更対象を指定。

テン*シー*シー-1

 2)変更対象が決まりEdit→Refactor→Rename…メニューが選べるようになってるので選択。

テン*シー*シー-2

 3)新しい名前を入力して実行(Rename related filesはチェックしておく )。

テン*シー*シー-3

 4)変更前後比較ウィンドウが出る。問題なければ実行。

テン*シー*シー-4

 5)スナップショットを取るか聞いてくるので、小心者はEnableだ。

テン*シー*シー-5

 6)そんな、ファイル名まで変わっている!便利~。

テン*シー*シー-6

 てな感じです。
 Refactorメニューで一番の目玉はConvert to Objective-C ARC…なんだけど、これはいずれ。
 後はThumbnailViewAppDelegate側の-application:didFinishLaunchingWithOptions:を以下のようにして、ようやくThumbnailViewControllerに画像ファイルパスの配列が渡るわけですな。
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[self buildDummys];
self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];

ThumbnailViewController* thumbnailViewController = [[[ThumbnailViewController alloc]
initWithNibName:@"ThumbnailViewViewController" bundle:nil] autorelease];
thumbnailViewController.images = [self images];
// Override point for customization after application launch.
self.viewController = thumbnailViewController;
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;
}

 あとはThumbnailViewController側の加工になります。
 まずは、素直に画像ファイルからUIImageを作って、その(228)でやったUIViewのようにタイル状に貼付けてみます。
 画像ファイルを読み込むのはそのフルパス文字列をUIImageの簡易コンストラクタ
+imageWithContentsOfFile:

 に指定してやればOK。作成されたUIImageは画像ファイルの画像ちゅーわけです。
 で、このUIImageを画面上に表示するにはいろいろな方法があるわけですが、今回はドリル本同様、UIImageViewという便利なUIViewサブクラスを使います。なんせ
imageプロパティ

 にUIImageを渡せばいいだけなんで。
UIImage* image = [UIImage imageWithContentsOfFile:filepath];
UIImageView* view = [[[UIImageView alloc]initWithFrame:CGRectInset(r, 4, 4)]autorelease];
view.image = image;

 とても簡単。
 で、あとはself.viewにaddSubviewしちゃえばいいわけです。
- (void)viewDidLoad
{
[super viewDidLoad];
CGRect r = CGRectMake(0, 0, 44, 44);
for (NSString* filepath in images) {
UIImage* image = [UIImage imageWithContentsOfFile:filepath];
UIImageView* view = [[[UIImageView alloc]initWithFrame:CGRectInset(r, 4, 4)]autorelease];
view.image = image;
[self.view addSubview:view];
r = CGRectOffset(r, r.size.width, 0);
if (r.origin.x > self.view.bounds.size.width) {
r.origin.x = 0;
r = CGRectOffset(r, 0, r.size.height);
}
}
}

 実行するとこんな感じにファイルを表示します。

テン*シー*シー-7

 適当に44 x 44のタイルにしたんでバリバリにはみ出してますな。
 それは後で調整するとして、先にちょっと問題点の検討に入ります。
 というのも、たまたまThumbnailViewAppDelegateで1000x1000の画像を10個程度にしてたからよかったんですが、1000x1000のカラー画像は4MB以上のメモリを必要とするので、これだけで40MBになっちゃってるんですよ。
 iPhone 3GSが確保できるギリの大きさに近い。
 シミュレータだと何ともないけど、実機だと無理~ってなるかもしれないんですな。
 大丈夫、俺iPhone 4Sだからとか言ってる人も、結局10個の画像が100個にでもなれば、たぶんアウトなわけですよ。

 この問題の一つの解決策として表示する大きさに画像を縮小する方法があるわけです。
 画像を縮小するには、一時的に画面外に描画エリア(以後オフスクリーンと言います)を用意して、そこにUIImageを縮小描画し、そのオフスクリーンからUIImageを作るってのが簡単です。
 実際のソースは以下のとおり
-(UIImage*)resizeImage:(UIImage*)image size:(CGSize)size
{
// 指定されたサイズのオフスクリーン作成。
UIGraphicsBeginImageContext(size);
// オフスクリーンいっぱいにimageを描画。
[image drawInRect:CGRectMake(0, 0, size.width, size.height)];
// オフスクリーンからUIImage作成。
UIImage* resizedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return resizedImage;
}

 UIImageに描画したい矩形を
-drawInRect:

 経由で渡す事で、現在描画対象になっているオフスクリーンに対し、その矩形におさまるように画像を描いてくれます。
UIGraphicsBeginImageContext

 が引数で指定した大きさのオフスクリーンの作成と現在の描画対象の切り替え作業をやってくれてます。
 で、オフスクリーンからUIImageの作成は
UIGraphicsGetImageFromCurrentImageContext

 を使い、用が無くなったオフスクリーンは
UIGraphicsEndImageContext

 で廃棄という段取りです。
- (void)viewDidLoad
{
・・・
UIImage* image = [UIImage imageWithContentsOfFile:filepath];
UIImageView* view = [[[UIImageView alloc]initWithFrame:CGRectInset(r, 4, 4)]autorelease];
view.image = [self resizeImage:image size:view.bounds.size];
[self.view addSubview:view];
・・・
}

 これで見た目は同じだけど、必要なメモリは44 x 44の画像10個だけって事になって100KBもあれば十分って事になります。
 1000個あっても8MBくらいで済むわけです。

 ただ~、結局のところ、これでもファイルが1万個くらいあると80MBいっちゃうわけですよ。
 ま、さすがに1万個は勘弁してよ、でもいいんですが、それ以前に画像を読み込んで縮小する作業は結構時間を食うわけでして、例えば200個の画像をiPhone実機で読み込ませて処理させるとどうなるかというと…

 ま、やってみりゃわかるのでThumbnailViewAppDelegateの-buildDummysメソッドのループを10から200に変えてRunしてみましょう。シミュレータでもかなりの時間食います。この間、まるでアプリケーションがハングアップしたような印象をユーザーに与えちゃうわけですな。
- (void)buildDummys
{
・・・
for (int i = 0; i < 200; i++) {
・・・
}

 ちなみに私のiMacで最初のダミー画像ファイル作成に20秒、その後の実行で5秒くらいかかります。

 Time Profilerを使うのじゃよ。Runのかわりにボタン長押しでメニュー表示させてProfile。

テン*シー*シー-8

 でてきたテンプレートでTime Profilerを選ぶ。

テン*シー*シー-9

 あとは画面が表示されるまで測定しておしまい。

テン*シー*シー-10

 なんすか、Time Profiler?って人はデバッグ本を読みましょう。ま、ちと古い情報の本になってしまったが…
 最新でしかもタダな方がいい人は日本語に翻訳されたiOSのドキュメントのInstrumentsユーザガイドやInstruments新機能ユーザガイドを読むべし!

 もちろん、最初のダミー画像ファイル作成は2度目のRunからは発生しないんだけど、それでも毎回、画面が表示されるまでに5秒かかる事になるんですな。それと、当然だけど画面からはみ出した部分は見ることができない。
 しかもThumbnailViewAppDelegate側の-application:didFinishLaunchingWithOptions:で間違ってThumbnailViewControllerのviewプロパティを触った日には5秒ルールで強制退場されるかもしれんわけですよ。ここらへんはドリルで話したとおりです。
 つーわけで、次回(たぶん年は越さないはず)はドリルの紹介を兼ねてUIScrollViewの導入と、いかにして即応性を向上させるかの話。

 ちなみにユーザーに、自分、仕事中ですってのをアピールするためには
UIActivityIndicatorView

 てのを使います。self.viewに貼付けて
-startAnimating

 を送ればグルグル回り初めて
-stopAnimating

 で画面から消える便利なやつ

$テン*シー*シー-13

 なんですが、-viewDidLoadメソッドで
- (void)viewDidLoad
{
[super viewDidLoad];
UIActivityIndicatorView* activityIndicatorView = [[[UIActivityIndicatorView alloc]
initWithFrame:CGRectMake((self.view.bounds.size.width - 44) / 2, 100, 44, 44)]
autorelease];
[self.view addSubview:activityIndicatorView];
[activityIndicatorView startAnimating];

・・・
画像読み込み、画面貼付け処理ループ
・・・
[activityIndicatorView stopAnimating];
}

 とやっても回ってくれません。
 というのも一度UIApplicationMainが管理するメインループに戻らないとアニメーション機能が働いてくれんのですよ。なのでNSObjectの
-performSelector:withObject:afterDelay:

 を使って時間差処理実行させたりします。
 -performSelector:withObject:はドリル本の主題のひとつでもあったわけですが、こいつは時間差処理実行にも利用できるんですな。afterDelay:の引数に遅らせたい秒数を指定します。今回の場合、単にメインループに戻したいだけなんで、遅らせる時間は0に指定。
 「画像読み込み、画面貼付け処理ループ」処理を一つのメソッド(-loadingimagesと命名)にまとめて、-performSelector:withObject:afterDelay:で時間差処理実行させるわけっす。

 あとUIActivityIndicatorViewは、貼付けるUIImageView群より手前にないと隠れてしまうので、そこらへんの対応も必要。
 といっても単にUIImageView群を貼付けるためのUIViewを用意するだけです。

$テン*シー*シー-14

 最後に-loadingimagesメソッドでは上記のUIViewへの-addSubview:と処理の終わりにUIActivityIndicatorViewに-stopAnimatingを送らないといけないんで、それぞれをインスタンス変数として持つことにします。
 ドリル本のようにtag使うのでもいいんですが、素直にインスタンス変数にします。

 ここで注目の機能がひとつ。
 Xcode 4以前に使われていたコンパイラのGCCだとインスタンス変数は@interface部で宣言しないと駄目だったんだけど、Apple LLVM(あとiOS 4以降限定だったかもしれんが)になって
@implementation ThumbnailViewController {
UIActivityIndicatorView* _activityIndicatorView;
UIView* _baseview;
}

 っていうように@implementationでも宣言できるようになったんですな。
 これができるとThumbnailViewController.h側に自分の実装詳細をさらさずに済むので非常にありがたいんですよ。
 公開用のヘッダーに、むやみに内部でのみ使う変数やメソッドをさらすと、何か変更するとそれを使ってる側も再コンパイルさせる事になるんで大規模開発ではこういう機能が重宝されるんですわ。

 これがドリルで言っていた本格的なカプセル化ちゅーやつです。
 @implementationではなく、クラス拡張を利用してThumbnailViewController.m側に
@interface ThumbnailViewController () { ← () て部分がクラス拡張の指定
UIActivityIndicatorView* _activityIndicatorView;
UIView* _baseview;
}
@end

 なんてしてもOKです。
 名前無しのカテゴリでは無い事に注意。
 実際の実装は以下のとおり。これで読み込み中はグルグルが回ります。
// 画像読み込み、貼付け処理
- (void)loadingimages
{
[[UIApplication sharedApplication] endIgnoringInteractionEvents];
CGRect r = CGRectMake(0, 0, 44, 44);
for (NSString* filepath in images) {
UIImage* image = [UIImage imageWithContentsOfFile:filepath];
UIImageView* view = [[[UIImageView alloc]initWithFrame:CGRectInset(r, 4, 4)]autorelease];
view.image = [self resizeImage:image size:view.bounds.size];
[_baseview addSubview:view];
r = CGRectOffset(r, r.size.width, 0);
if (r.origin.x > _baseview.bounds.size.width) {
r.origin.x = 0;
r = CGRectOffset(r, 0, r.size.height);
}
}
[_activityIndicatorView stopAnimating];
}

- (void)viewDidLoad
{
[super viewDidLoad];

// 先に画像用のベース作成、貼付け
_baseview = [[[UIView alloc] initWithFrame:self.view.bounds]autorelease];
[self.view addSubview:_baseview];

// インジケータ貼付け
_activityIndicatorView = [[[UIActivityIndicatorView alloc]
initWithFrame:CGRectMake((self.view.bounds.size.width - 44) / 2, 100, 44, 44)]
autorelease];
[self.view addSubview:_activityIndicatorView];

// グルグル開始
[_activityIndicatorView startAnimating];

// 画像読み込み処理時間差実行
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[[UIApplication sharedApplication] beginIgnoringInteractionEvents];
[self performSelector:@selector(loadingimages) withObject:nil afterDelay:0];
}

 +cancelPreviousPerformRequestsWithTarget:なんかで、以前の時差処理リクエストを解除するなどの調整や-beginIgnoringInteractionEventsでユーザーからの入力を止めておいて-loadingimages側で-endIgnoringInteractionEventsなんてやってますが、今回の場合、あんま必要は無いですね。
 何やってるかは各自で調べてちょ。

------------
サンプルプロジェクト:ThumbnailView-2.zip