Autolayout で UITableViewCell の高さを設定すると reloadData 後にカクカクすることがある

# 未だセルの高さ計算処理は面倒くさい気がする

![cells.png](https://qiita-image-store.s3.amazonaws.com/0/3603/1d878a3a-42b2-ac56-d5ce-6cf6954fd880.png)

UITableViewCell の高さを動的に計算する（セルの中身に合わせてセルの高さが変わる）という仕組みの実装はある意味で iOS プログラミングの定石かつ鬼門な気がしてなりません。ベストソリューションがあれば是非教えて下さい。

iOS 8からは Self Sizing Cells という方法が登場し、 Autolayout+UITableViewAutomaticDimension でセルの高さを自動計算してくれるようになりました。
> Understanding Self Sizing Cells and Dynamic Type in iOS 8
> http://www.appcoda.com/self-sizing-cells/

ただ、これを実際に実装してみると「reloadData 後にスクロールがカクカクする」という現象に見舞われます。描画のコマ落ちというよりかは、単純に座標がずれていっているような感じです。

**前提**

* セルは文字列量に合わせて高さが動的に変化する
* Autolayout+UITableViewAutomaticDimension でセルの高さを自動計算
* スクロールを検証したいので dataSource は多めに

**再現手順**

* 一度目の reloadData 後、下までスクロール
* dataSource 末尾にデータを複数追加して再度 reloadData
* 追加されたセルを表示するために下方向へ更にスクロール
* ここで上方向にスクロールしていくと座標がずれるのかカクカクする

# セルの高さを概算値にするアレが怪しい

iOS 7 からは `estimatedRowHeight` であらかじめセルの高さの概算値を設定しておいて実際の計算処理を遅延させる仕組みが出来ましたが、どうもこの estimatedRowHeight と実際の高さが異なると「reloadData 後にスクロールがカクカクする」現象が起こるように見えます。

# ひとつの回避策

「estimatedRowHeight と実際の高さが異なる」のが原因であれば、では「estimatedRowHeight を実際の高さに合わせればよいのでは」という仮説を立てて対策してみたところ、見事にカクカクが無くなり滑らかにスクロールするようになったのでここに記しておきます。そもそも Autolayout の組み方が悪かったとか、その他に原因があったとしたらすみません。

セルの高さがしょっちゅう変化するものでもなければ、一度セルの高さが確定したらそれを保持しておいて、次からはそれを `-tableView:estimatedHeightForRowAtIndexPath:` で返却するようにしてやればよいのです。

```objc:UITableViewDelegate
@interface MyClass () <UITableViewDelegate>

@property (strong, nonatomic) NSMutableDictionary *cellHeights;

@end
```
```objc:UITableViewDelegate
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 100;
self.cellHeights = @{}.mutableCopy;
```
```objc:UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
	NSString *key = [indexPath stringValue]; // 独自のカテゴリを実装しています
	NSNumber *height = self.cellHeights[key];
	
	if (height) {
		return [height doubleValue];
	}
	
	return tableView.estimatedRowHeight;
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
	[cell.contentView updateConstraints];

	// 一度決定したセルの高さを NSDictionary に保持しておく
	NSString *key = [indexPath stringValue]; // 独自のカテゴリを実装しています
	if (!self.cellHeights[key]) {
		self.cellHeights[key] = @(cell.frame.size.height);
	}
}
```

ただし、セルの初回表示の際のスクロールインジケータの方のカクつきまでは対応できませんでした。そもそも概算値で contentSize を見積もっているのでこれは仕方ないのかもしれません。
また、画面回転等でセルの高さが変化する場合にはこれだけでは対処しきれません。

# まとめ

Autolayout+UITableViewAutomaticDimension で UITableView を組む際に、reloadData 後にも同じ内容のセルを表示する場合には以下の点に注意する。

* `-tableView:willDisplayCell:forRowAtIndexPath:` で決定したセルの高さ値を例えば NSIndexPath をキーとして保持しておく
* 保持していた高さ値を `-tableView:estimatedHeightForRowAtIndexPath:` で返却する。初回表示であればデフォルトの `estimatedRowHeight` を返却する
* なお、NSIndexPath←→NSString は標準 API には無い機能なのでカテゴリを作って置くと便利

https://github.com/usagimaru/SelfSizingCells

# 参考

* [別の対処法もあるようです](http://qiita.com/hirobe/items/27314daa278761d2ab61)

* [Understanding Self Sizing Cells and Dynamic Type in iOS 8](http://www.appcoda.com/self-sizing-cells/)

* [文字列の行数によって高さが変わる UITableViewCell の実装方法](http://qiita.com/CarmineScarlet/items/68bcdb2d7dcf7ae9e96a)