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

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

UITableViewから渡されるindexPath、実は2種類ある。
NSIndexPathクラス以外に、UIMutableIndexPathというクラスが利用されることがある。

* `tableView:estimatedHeightForRowAtIndexPath:`で渡されるindexPathは`UIMutableIndexPath`クラスのインスタンス
* `tableView:heightForRowAtIndexPath:`や`tableView:cellForRowAtIndexPath:`で渡されるindexPathは`NSIndexPath`クラスのインスタンス

つまり、「`indexPath`をそのままNSDictionaryのキーとして高さをキャッシュしておく」みたいな処理をしていると意図しない動きをすることがある。
具体的には、`isEqual`が`NO`になることがあるため、入れたはずの値`objectForKey:`で取り出せない。
これ、コード的には問題なく見えるため、ハマると原因がわかりづらくなかなか厄介なやつ。日本語の情報とか見たことないし。。。

# 実際に確認


```objc

- (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](https://qiita-image-store.s3.amazonaws.com/0/92660/7b77cb13-25e9-443e-9fea-03d6a12d9bf7.png)

* `tableView:cellForRowAtIndexPath:`で渡されるindexPathは`NSIndexPath`クラス
![01-2.png](https://qiita-image-store.s3.amazonaws.com/0/92660/02716e2f-a518-8459-e379-2d82e9c2697a.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`クラスを指定することで、「入れたはずの値が取り出せない」ということがなくなる。

```objc

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

# 追記(2015/09/12)

[tomohisaota](http://qiita.com/tomohisaota)さんの[コメント](http://qiita.com/rhoboro/items/89e3d1cfe9395effa88d#comment-102b6c20a251cc512dac)を受けて`copy`を試したところ、iOS7でもすべて「キャッシュあり」が出力されました。
もちろん、[usagimaru](http://qiita.com/usagimaru)さんの[コメント](http://qiita.com/rhoboro/items/89e3d1cfe9395effa88d#comment-90c6bcc58a75f078f948)にあるようにNSStringとの相互変換メソッドを用意するのもいいと思います。
3つの中では`copy`メソッドを利用する方法が一番無駄がなさそうですね。
tomohisaotaさん、usagimaruさん貴重なアドバイスありがとうございます。

```diff
 - (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;
}
```