Edited at

UITableViewから渡されるindexPathをそのままキーにしてはいけない

More than 3 years have passed since last update.


UITableViewから渡されるindexPathをそのままキーにしてはいけない

UITableViewから渡されるindexPath、実は2種類ある。

NSIndexPathクラス以外に、UIMutableIndexPathというクラスが利用されることがある。



  • tableView:estimatedHeightForRowAtIndexPath:で渡されるindexPathはUIMutableIndexPathクラスのインスタンス


  • tableView:heightForRowAtIndexPath:tableView:cellForRowAtIndexPath:で渡されるindexPathはNSIndexPathクラスのインスタンス

つまり、「indexPathをそのままNSDictionaryのキーとして高さをキャッシュしておく」みたいな処理をしていると意図しない動きをすることがある。

具体的には、isEqualNOになることがあるため、入れたはずの値objectForKey:で取り出せない。

これ、コード的には問題なく見えるため、ハマると原因がわかりづらくなかなか厄介なやつ。日本語の情報とか見たことないし。。。


実際に確認


- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.items count];
}

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
if (![self.heightDic objectForKey:indexPath]) {
// indexPathをキーにしてセルの高さをキャッシュに保存
NSLog(@"S: %ld, R: %ld に30.0をセット", indexPath.section, indexPath.row);
[self.heightDic setObject:@30 forKey:indexPath];
return 30.0f;
}
return 44.0f;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 30.0f;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
if (cell == nil) {
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"Cell"];
cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
}

// indexPathをキーにして、キャッシュが取り出せるか否かを確認
if ([self.heightDic objectForKey:indexPath]) {
NSLog(@"S: %ld, R: %ld はキャッシュあり", indexPath.section, indexPath.row);
cell.textLabel.text = @"キャッシュあり";
} else {
NSLog(@"S: %ld, R: %ld はキャッシュなし", indexPath.section, indexPath.row);
cell.textLabel.text = @"キャッシュなし";
}

return cell;
}


Xcodeで見てみた


  • tableView:estimatedHeightForRowAtIndexPath:で渡されたindexPathはUIMutableIndexPathクラスになっている

    01-1.png


  • tableView:cellForRowAtIndexPath:で渡されるindexPathはNSIndexPathクラス

    01-2.png



ログ

2015-09-12 02:34:17.689 IndexPathBehavior[66159:607] S: 0, R: 0 に30.0をセット

2015-09-12 02:34:17.689 IndexPathBehavior[66159:607] S: 0, R: 1 に30.0をセット
2015-09-12 02:34:17.690 IndexPathBehavior[66159:607] S: 0, R: 2 に30.0をセット
2015-09-12 02:34:17.690 IndexPathBehavior[66159:607] S: 0, R: 3 に30.0をセット
2015-09-12 02:34:17.690 IndexPathBehavior[66159:607] S: 0, R: 4 に30.0をセット
2015-09-12 02:34:17.691 IndexPathBehavior[66159:607] S: 0, R: 5 に30.0をセット
2015-09-12 02:34:17.691 IndexPathBehavior[66159:607] S: 0, R: 6 に30.0をセット
2015-09-12 02:34:17.691 IndexPathBehavior[66159:607] S: 0, R: 7 に30.0をセット
2015-09-12 02:34:17.692 IndexPathBehavior[66159:607] S: 0, R: 8 に30.0をセット
2015-09-12 02:34:17.692 IndexPathBehavior[66159:607] S: 0, R: 9 に30.0をセット
2015-09-12 02:34:17.692 IndexPathBehavior[66159:607] S: 0, R: 10 に30.0をセット
2015-09-12 02:34:17.692 IndexPathBehavior[66159:607] S: 0, R: 11 に30.0をセット
2015-09-12 02:34:17.693 IndexPathBehavior[66159:607] S: 0, R: 12 に30.0をセット
2015-09-12 02:34:17.693 IndexPathBehavior[66159:607] S: 0, R: 13 に30.0をセット
2015-09-12 02:34:17.693 IndexPathBehavior[66159:607] S: 0, R: 14 に30.0をセット
2015-09-12 02:34:17.694 IndexPathBehavior[66159:607] S: 0, R: 15 に30.0をセット
2015-09-12 02:34:17.694 IndexPathBehavior[66159:607] S: 0, R: 16 に30.0をセット
2015-09-12 02:34:17.694 IndexPathBehavior[66159:607] S: 0, R: 17 に30.0をセット
2015-09-12 02:34:17.695 IndexPathBehavior[66159:607] S: 0, R: 18 に30.0をセット
2015-09-12 02:34:17.695 IndexPathBehavior[66159:607] S: 0, R: 19 に30.0をセット
2015-09-12 02:34:17.695 IndexPathBehavior[66159:607] S: 0, R: 20 に30.0をセット
2015-09-12 02:34:17.696 IndexPathBehavior[66159:607] S: 0, R: 21 に30.0をセット
2015-09-12 02:34:17.696 IndexPathBehavior[66159:607] S: 0, R: 22 に30.0をセット
2015-09-12 02:34:17.696 IndexPathBehavior[66159:607] S: 0, R: 23 に30.0をセット
2015-09-12 02:34:17.697 IndexPathBehavior[66159:607] S: 0, R: 24 に30.0をセット
2015-09-12 02:34:17.697 IndexPathBehavior[66159:607] S: 0, R: 25 に30.0をセット
2015-09-12 02:34:17.704 IndexPathBehavior[66159:607] S: 0, R: 26 に30.0をセット
2015-09-12 02:34:17.704 IndexPathBehavior[66159:607] S: 0, R: 27 に30.0をセット
2015-09-12 02:34:17.705 IndexPathBehavior[66159:607] S: 0, R: 28 に30.0をセット
2015-09-12 02:34:17.705 IndexPathBehavior[66159:607] S: 0, R: 29 に30.0をセット
2015-09-12 02:34:17.706 IndexPathBehavior[66159:607] S: 0, R: 0 はキャッシュあり
2015-09-12 02:34:17.707 IndexPathBehavior[66159:607] S: 0, R: 1 はキャッシュあり
2015-09-12 02:34:17.708 IndexPathBehavior[66159:607] S: 0, R: 2 はキャッシュあり
2015-09-12 02:34:17.709 IndexPathBehavior[66159:607] S: 0, R: 3 はキャッシュあり
2015-09-12 02:34:17.709 IndexPathBehavior[66159:607] S: 0, R: 4 はキャッシュあり
2015-09-12 02:34:17.709 IndexPathBehavior[66159:607] S: 0, R: 5 はキャッシュあり
2015-09-12 02:34:17.710 IndexPathBehavior[66159:607] S: 0, R: 6 はキャッシュあり
2015-09-12 02:34:17.710 IndexPathBehavior[66159:607] S: 0, R: 7 はキャッシュあり
2015-09-12 02:34:17.725 IndexPathBehavior[66159:607] S: 0, R: 8 はキャッシュあり
2015-09-12 02:34:17.727 IndexPathBehavior[66159:607] S: 0, R: 9 はキャッシュあり
2015-09-12 02:34:17.728 IndexPathBehavior[66159:607] S: 0, R: 10 はキャッシュあり
2015-09-12 02:34:17.729 IndexPathBehavior[66159:607] S: 0, R: 11 はキャッシュあり
2015-09-12 02:34:17.730 IndexPathBehavior[66159:607] S: 0, R: 12 はキャッシュあり
2015-09-12 02:34:17.731 IndexPathBehavior[66159:607] S: 0, R: 13 はキャッシュあり
2015-09-12 02:34:17.732 IndexPathBehavior[66159:607] S: 0, R: 14 はキャッシュあり
2015-09-12 02:34:17.733 IndexPathBehavior[66159:607] S: 0, R: 15 はキャッシュあり
2015-09-12 02:34:21.369 IndexPathBehavior[66159:607] S: 0, R: 16 はキャッシュあり
2015-09-12 02:34:21.538 IndexPathBehavior[66159:607] S: 0, R: 17 はキャッシュあり
2015-09-12 02:34:21.779 IndexPathBehavior[66159:607] S: 0, R: 18 はキャッシュあり
2015-09-12 02:34:22.138 IndexPathBehavior[66159:607] S: 0, R: 19 はキャッシュあり
2015-09-12 02:34:22.154 IndexPathBehavior[66159:607] S: 0, R: 20 はキャッシュなし
2015-09-12 02:34:22.171 IndexPathBehavior[66159:607] S: 0, R: 21 はキャッシュなし
2015-09-12 02:34:22.188 IndexPathBehavior[66159:607] S: 0, R: 22 はキャッシュなし
2015-09-12 02:34:22.237 IndexPathBehavior[66159:607] S: 0, R: 23 はキャッシュなし
2015-09-12 02:34:22.505 IndexPathBehavior[66159:607] S: 0, R: 24 はキャッシュなし
2015-09-12 02:34:22.573 IndexPathBehavior[66159:607] S: 0, R: 25 はキャッシュなし
2015-09-12 02:34:22.641 IndexPathBehavior[66159:607] S: 0, R: 26 はキャッシュなし
2015-09-12 02:34:23.428 IndexPathBehavior[66159:607] S: 0, R: 27 はキャッシュなし
2015-09-12 02:34:23.554 IndexPathBehavior[66159:607] S: 0, R: 28 はキャッシュなし
2015-09-12 02:34:23.687 IndexPathBehavior[66159:607] S: 0, R: 29 はキャッシュなし


結果

最初に30セル分の高さをNSMutableDictionaryに格納している。

しかし、20セル目以降は[self.heightDic objectForKey:indexPath]でキャッシュしたはずの値が取り出せず「キャッシュなし」と出力されている。

ちなみにこれはXcode6, iOS7で確認した挙動。iOS8ではすべて「キャッシュあり」が出力された。

OSバージョンで挙動が変わるのも、この問題の厄介な点の一つ。


対応方法

セルの高さを複雑な計算で動的に変えようとすると、パフォーマンスの面からキャッシュしておきたくなることは多い。

そんなときは受け取ったindexPathから自分で作り直せば問題ない。

自分でNSIndexPathクラスを指定することで、「入れたはずの値が取り出せない」ということがなくなる。


// 受け取ったindexPathと同じ位置を指すNSIndexPathを作成
NSIndexPath *key = [NSIndexPath indexPathForRow:indexPath.row inSection:indexPath.section];


追記(2015/09/12)

tomohisaotaさんのコメントを受けてcopyを試したところ、iOS7でもすべて「キャッシュあり」が出力されました。

もちろん、usagimaruさんのコメントにあるようにNSStringとの相互変換メソッドを用意するのもいいと思います。

3つの中ではcopyメソッドを利用する方法が一番無駄がなさそうですね。

tomohisaotaさん、usagimaruさん貴重なアドバイスありがとうございます。

 - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {

if (![self.heightDic objectForKey:indexPath]) {
NSLog(@"S: %ld, R: %ld に30.0をセット", indexPath.section, indexPath.row);
- [self.heightDic setObject:@30 forKey:indexPath];
+ [self.heightDic setObject:@30 forKey:[indexPath copy]];
return 30.0f;
}
return 44.0f;
}