ImageStoreはiOSで画像を非同期ダウンロードするためのObjective-Cライブラリです。Satoshi Nakagawaさんがオープンソースのライブラリとして公開されています(New BSDライセンス)。手軽に使えて非常に便利なライブラリなのですが、そのままアプリに組み込むには少々難があります。
- ダウンロードをキャンセルする手段がない
- ImageStoreのインスタンスを破棄する以外に、キャッシュをクリアする方法がない
- 正常に画像がダウンロードできたかどうか判定する術がない
1がないので、ダウンロード中に画面移動が発生したりすると容易にクラッシュします。2がないので、随時キャッシュを消して再ダウンロードしたり、メモリ不足時にキャッシュを削除したりといったことができません。3のため、サーバが404を返した場合等にはリクエストが無限ループしてしまう危険があります(ImageStoreの中に含まれるサンプルアプリではそういう仕様になってしまっています)。また画像が取得できなかった時に代替画像を表示するといった処理も実装できません。
そこでオリジナル版からforkして、これらの不具合を解消した改造版を作りました。
https://github.com/tkyk/imagestore
(4月6日追記: 私の改造版とは使い方が異なりますが、本家ImageStoreでも同等の修正が行われました)
ImageStoreクラスには4つのメソッドが増えました。どんなメソッドかは名前から想像がつくと思います。
- (void)cancelAllDownloads;
- (void)cancelDownloadFromUrl:(NSString*)url;
- (void)clearAllCaches;
- (void)clearCacheForUrl:(NSString*)url;
ImageStoreDelegateにも新たにメソッドが宣言されています。
- (void)imageStoreDidFailNewImage:(ImageStore *)sender
url:(NSString *)url
fallbackImage:(UIImage**)anImage;
使い方
UITableViewの各Cellに画像を表示する処理を例に、使い方を説明していきます。現実のiPhoneアプリの開発で良くあるパターンだと思います。
まず tableView:cellForRowAtIndexPath: で UITableViewCell の表示を更新するときにgetImageを呼び出します。画像が既にキャッシュされていればその場でUIImageのインスタンスが返却され、キャッシュがなければnilが返されて非同期の画像ダウンロードが開始されます。
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell;
//...略...
//このindexPathに対応する画像のURL
NSString *imageURL = [self getImageURLAtIndexPath: indexPath];
//getImageにURLを渡す
cell.imageView.image = [imageStore getImage: imageURL];
return cell;
}
画像が正常に取得できるとImageStoreDelegateの imageStoreDidGetNewImage:url: が呼ばれるので、reloadDataを呼び出してセルの表示を更新します。これによって再度getImageが呼ばれ、キャッシュから画像が取り出されます。
- (void)imageStoreDidGetNewImage:(ImageStore *)sender url:(NSString *)url
{
[self.tableView reloadData];
}
ここまではオリジナル版のImageStoreと同じ流れです。
しかしオリジナル版では画像の取得に失敗した場合にもこのメソッドが呼ばれてしまうため、リクエストが(成功するまで)無限にループしてしまいます。そこで私の改造版では画像の取得に失敗した場合、具体的には
- ステータスコードが200番台では無い
- MIMETypeがimage/*ではない
- UIImageのinitWithDataがnilを返した
こういった場合、 代わりに imageStoreDidFailNewImage:url:fallbackImage: が呼び出されます。
// 代替となる画像(fallbackImage)を事前に読み込んでおく
- (id)initWithCoder:(NSCoder *)aDecoder
{
//..略
fallbackImage = [[UIImage imageNamed:@"no_image"] retain];
return self;
}
// 読み込み失敗時にはこのメソッドが呼ばれるので、
// fallbackImageを代替画像として使用
- (void)imageStoreDidFailNewImage:(ImageStore *)sender
url:(NSString *)url
fallbackImage:(UIImage **)anImage
{
*anImage = fallbackImage;
}
第一引数はImageStoreのインスタンス、第二引数は失敗したURLで、第三引数は代替画像(UIImageのインスタンス)に対するポインタです。このポインタに非ゼロの値をセットしておくと、その画像が代わりにキャッシュされた上で imageStoreDidGetNewImage:url: が呼び出されます。つまり「正常に取得できなかった」という結果をキャッシュすることができる、ということになります。
キャッシュのクリア
一般に、didReceiveMemoryWarningでは全画像キャッシュをクリアしておくべきでしょう。
- (void)didReceiveMemoryWarning
{
[imageStore clearAllCaches];
[super didReceiveMemoryWarning];
}
その他に、ダウンロードに失敗した画像のURLを記録しておいて、「再読込」操作時にキャッシュをクリアして再リクエストする、といった用途も考えられます。
参考: ImageStoreのコードリーディング – haoyayoi Dev Style
ダウンロードのキャンセル
多くの場合、viewWillDisappear:のタイミングで全ての画像のダウンロードをキャンセルするのが良いだろうと思います。
- (void)viewWillDisappear:(BOOL)animated
{
[imageStore cancelAllDownloads];
[super viewWillDisappear:animated];
}
ただしこのままだとダウンロード中に別の画面に移動した場合、ダウンロードが完了していなかった画像がそのままになってしまうので、戻ってきたときに自動でダウンロードが再開されるよう、viewWillAppear:でセルの表示を更新する必要があります。
- (void)viewWillAppear:(BOOL)animated
{
[self.tableView reloadData];
//またはvisibleCellsを辿って表示を更新するなど
//for(UITableViewCell *cell in [self.tableView visibleCells])
[super viewWillAppear:animated];
}