セッション211-iOSでの同時ユーザーインターフェイスの構築(WWDC 2012から)では、バックグラウンドタスクが追いつくにつれてセルの画像が変化する問題について説明します(38分15秒から開始)。
この問題を解決する方法は次のとおりです。バックグラウンドキューに画像を読み込んだ後、インデックスパスを使用して、その画像を含むアイテムを表示している現在のセル(存在する場合)を見つけます。セルを取得したら、セルの画像を設定します。nilを取得した場合、そのアイテムを現在表示しているセルはないため、画像を破棄するだけです。
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MyCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
cell.imageView.image = nil;
dispatch_async(myQueue, ^{
[self bg_loadImageForRowAtIndexPath:indexPath];
});
return cell;
}
- (void)bg_loadImageForRowAtIndexPath:(NSIndexPath *)indexPath {
// I assume you have a method that returns the path of the image file. That
// method must be safe to run on the background queue.
NSString *path = [self bg_pathOfImageForItemAtIndexPath:indexPath];
UIImage *image = [UIImage imageWithContentsOfFile:path];
// Make sure the image actually loads its data now.
[image CGImage];
dispatch_async(dispatch_get_main_queue(), ^{
[self setImage:image forItemAtIndexPath:indexPath];
});
}
- (void)setImage:(UIImage *)image forItemAtIndexPath:(NSIndexPath *)indexPath {
MyCell *cell = (MyCell *)[collectionView cellForItemAtIndexPath:indexPath];
// If cell is nil, the following line has no effect.
cell.imageView.image = image;
}
これは、ビューが不要になった画像を読み込むバックグラウンドタスクの「積み重ね」を減らす簡単な方法です。NSMutableSet
インスタンス変数を自分に与えます:
@implementation MyViewController {
dispatch_queue_t myQueue;
NSMutableSet *indexPathsNeedingImages;
}
画像を読み込む必要がある場合は、セルのインデックスパスをセットに入れ、セットから1つのインデックスパスを取り出してその画像を読み込むタスクをディスパッチします。
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MyCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
cell.imageView.image = nil;
[self addIndexPathToImageLoadingQueue:indexPath];
return cell;
}
- (void)addIndexPathToImageLoadingQueue:(NSIndexPath *)indexPath {
if (!indexPathsNeedingImages) {
indexPathsNeedingImages = [NSMutableSet set];
}
[indexPathsNeedingImages addObject:indexPath];
dispatch_async(myQueue, ^{ [self bg_loadOneImage]; });
}
セルが表示されなくなった場合は、セルのインデックスパスをセットから削除します。
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
[indexPathsNeedingImages removeObject:indexPath];
}
画像を読み込むには、セットからインデックスパスを取得します。セットの競合状態を回避するために、このためにメインキューにディスパッチする必要があります。
- (void)bg_loadOneImage {
__block NSIndexPath *indexPath;
dispatch_sync(dispatch_get_main_queue(), ^{
indexPath = [indexPathsNeedingImages anyObject];
if (indexPath) {
[indexPathsNeedingImages removeObject:indexPath];
}
}
if (indexPath) {
[self bg_loadImageForRowAtIndexPath:indexPath];
}
}
方法は上記bg_loadImageForRowAtIndexPath:
と同じです。ただし、実際にセルに画像を設定するときは、セットからセルのインデックスパスも削除する必要があります。
- (void)setImage:(UIImage *)image forItemAtIndexPath:(NSIndexPath *)indexPath {
MyCell *cell = (MyCell *)[collectionView cellForItemAtIndexPath:indexPath];
// If cell is nil, the following line has no effect.
cell.imageView.image = image;
[indexPathsNeedingImages removeObject:indexPath];
}