LoginSignup
36
35

More than 5 years have passed since last update.

tableView:cellForRowAtIndexPath:でGCDのBlocksにキャプチャするのはcellではなくindexPathが正解だった

Last updated at Posted at 2013-07-19

長いタイトルですが。。
UITableViewCellに画像を表示する時に、画像の読み込みに時間がかかるので、GCDで非同期処理をする時の話です。

画像を表示するのがcellなので、次のようなコードを書いていたのですが、これは間違いでした。
このコードでは、UITableViewが高速にスクロールされた時などに、画像の読み込みが終わる前に画面上からいなくなり、Re-Useされることがあります。そうなってしまうと、Re-Useされる前に表示するはずだった画像がRe-Use後に表示する画像の前に一瞬だけ表示されてしまい、見た目がちょっとかっこ悪くなります。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    NSString *path = self.pathArray[indexPath.row];
    dispatch_queue_t q_global = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(q_global, ^{
        // 重い処理
        UIImage *image = [[UIImage alloc] initWithContentsOfFile:path];
        dispatch_queue_t q_main = dispatch_get_main_queue();
        dispatch_async(q_main, ^{
            // cellをキャプチャ
            // -> 重い処理の後には、別のindexPathのcellかもしれない
            cell.imageView.image = image;
        });
    });
}

何か良い方法はないものかと調べていたら、このブログ記事を見つけました。→UITableViewCellで画像を非同期でロードしようとしたときに困った事②
この記事を読んでいて、cellではなくindexPathをキャプチャすれば解決することに気づきました。
ソースコードを書き直すと、次のようになります。このコードであれば、目的のindexPathのcellに画像を表示することができます。
なお、cellForRowAtIndexPath:メソッドは、indexPathが示すcellが画面上に表示されていなければnilを返します。

[2013/7/21 0:35 追記]
UITableView#cellForRowAtIndexPath:だと、画面上に表示されていてもnilが返される事があるとのコメントを頂きました。
画面上のcellにもれなく画像を表示できるよう、indexPathだけでなくcellもキャプチャするようソースを修正しました。
また、cellに現在のindexPathを持たせるために、カテゴリを使用してUITableViewCellクラスを拡張しています。

UITableViewCell+CurrentIndexPath.h
#import <UIKit/UIKit.h>

// UITableViewCellクラスを拡張し、currentIndexPathプロパティを追加
@interface UITableViewCell (CurrentIndexPath)

@property (nonatomic, strong) NSIndexPath *currentIndexPath;

@end
UITableViewCell+CurrentIndexPath.m
#import "UITableViewCell+CurrentIndexPath.h"
#import <objc/runtime.h>

static char kCurrentIndexPathKey;

// currentIndexPathプロパティの実装
@implementation UITableViewCell (CurrentIndexPath)

// getter
- (NSIndexPath *)currentIndexPath
{
    return objc_getAssociatedObject(self, &kCurrentIndexPathKey);
}

// setter
- (void)setCurrentIndexPath:(NSIndexPath *)currentIndexPath
{
    objc_setAssociatedObject(self, &kCurrentIndexPathKey, currentIndexPath, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end
// #import "UITableViewCell+CurrentIndexPath.h"

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    // cellをデキューしたら、現在のindexPathを持たせる
    cell.currentIndexPath = indexPath
    NSString *path = self.pathArray[indexPath.row];
    dispatch_queue_t q_global = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(q_global, ^{
        // 重い処理
        UIImage *image = [[UIImage alloc] initWithContentsOfFile:path];
        dispatch_queue_t q_main = dispatch_get_main_queue();
        dispatch_async(q_main, ^{
            // cellとindexPathをキャプチャ
            //  -> cellが持っているindexPathとキャプチャしたindexPathが等しい場合のみ画像をセット
            if ([cell.currentIndexPath isEqual:indexPath]) {
                cell.imageView.image = image;
            }
        });
    });
}
36
35
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
35