文字列の行数によって高さが変わる UITableViewCell の実装方法（Autolayout／自前計算）

![cells.png](https://qiita-image-store.s3.amazonaws.com/0/3603/accb58cf-ae32-53cb-833d-70517484f1b3.png)

Twitter のように文字列の長さ・行数によって高さが変わる UITableViewCell の実装方法です。

# iOS 8 + Autolayout

iOS 8 からは Self Sizing Cells という方法で計算処理はすべて Autolayout システムに任せられるようになったので、かつてのような面倒なやり方は不要です。
UITableViewCell を Storyboard や xib でレイアウトする際に適切な制約を付けておき、UITableView.rowHeight に `UITableViewAutomaticDimension` を指定しておけば良いみたいです。

```
tableView.rowHeight = UITableViewAutomaticDimension;
```

![l.png](https://qiita-image-store.s3.amazonaws.com/0/3603/fb6cd9f8-66d9-c709-bf13-411c722d5e70.png)

UITableViewDataDelegate の `-tableView:heightForRowAtIndexPath:` `-tableView:estimatedHeightForRowAtIndexPath:` は実装しなくてもこれだけで自動で高さを計算してくれます。

## ソースコード

https://github.com/usagimaru/SelfSizingCells

## 関連記事

Autolayout で UITableViewCell の高さを設定すると reloadData 後にカクカクすることがある
http://qiita.com/usagimaru/items/664d0d67fe6f8f7fafed


# 古いやり方

Autosizing + 高さ自前計算

## UITableViewDataSource / UITableViewDelegate

```objc:UITableViewDataSource/UITableViewDelegate

// データソース数
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return self.items.count;
}

// セル生成
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	NSString *identifier = @"cell";
	// 使い回しセルの取得を試みる
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
	
	// セルを新たに生成
	if (!cell)
	{
		// セルを新たに生成
		cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
		
		// カスタムセルの中身
		MyCellContentView *contentView = [MyCellContentView cellContentView];
		
		[cell.contentView addSubview:contentView];
	}
	
	// contentViewに乗っけているカスタムセルの中身を取得
	// サブビューは1つしかないという前提
	MyCellContentView *contentView = [cell.contentView subviews][0];
	
	// データソースの値を反映
	contentView.text = self.items.text;

	return cell;
}

// セルの高さを返す. セルが生成される前に実行されるので独自に計算する必要がある
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{	
	MyItem *item = [self.items objectAtIndex:indexPath.row];
	// カスタムセルのクラスメソッド内で高さを計算. 後述
	CGFloat cellHeight = [MyCellContentView cellHeightWithText:[item text]];
	
	return cellHeight;
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
	// contentViewに乗っけているサブビュー, カスタムセルの中身を取得
	MyCellContentView *contentView = [[cell.contentView subviews] objectAtIndex:0]; // サブビューは1つしかないという前提
		
	// ここでカスタムセルの中身のframeを再設定する
	cell.contentView.frame = cell.bounds; // iOS 6 以降では必要みたい
	contentView.frame = cell.bounds;
}

```

## カスタムセルの中身と高さ計算

```objc:MyCellContentView.h

@interface MyCellContentView : UIView

@property (copy, nonatomic) NSString *text;

// 初期化
+ (instancetype)cellContentView;

// text が収まるのに必要な高さを計算して返す
+ (CGFloat)cellHeightWithText:(NSString*)text;

@end
```

```objc:MyCellContentView.m

#import "MyCellContentView.h"

@interface MyCellContentView ()

@property (weak, nonatomic) IBOutlet BALabel *textLabel;

@end

@implementation MyCellContentView

// セルの高さを計算する際に使う
static CGFloat _standardCellHeight;
static CGRect _standardLabelRect;
static NSLineBreakMode _labelLineBreakMode;
static UIFont *_labelFont;

+ (void)initialize
{
	[super initialize];
	
	if (self == [MyCellContentView class])
	{
		// セルの高さを計算する際に使う情報を保持しておく
		MyCellContentView *view = [MyCellContentView cellContentView];
		
		_standardCellHeight = CGRectGetHeight(view.frame); // xib の時点でのセルの高さ. 基本これが最小値
		_standardLabelRect = view.textLabel.frame; // xib の時点でのラベルの frame
		_labelLineBreakMode = view.textLabel.lineBreakMode; // xib の時点でのラベルの LineBreakMode
		_labelFont = view.textLabel.font; // xib の時点でのラベルのフォント
	}
}

// nib(xib)がロードされたときに呼ばれる
- (void)awakeFromNib
{
	[super awakeFromNib];
	
	self.text = self.text; // 値を更新する
}

+ (instancetype)cellContentView
{
	UINib *nib = [UINib nibWithNibName:@"MyCellContentView" bundle:nil];
	return [nib instantiateWithOwner:self options:nil][0];
}

// 文字列が収まるのに必要な高さを計算して返す
+ (CGFloat)cellHeightWithText:(NSString*)text
{
	if (!text || text.length == 0) {
		return _standardCellHeight;
	}
	
	// 横幅を適宜指定
	CGFloat width = _standardLabelRect.size.width;
	
	// 文字列が描画される矩形の大きさを取得
	CGSize size = [self boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX)
								   	 options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine
							   	  attributes:@{NSFontAttributeName : _labelFont}
									 context:nil].size;
	
	// 余白とか
	CGFloat margin = fabs(_standardCellHeight - CGRectGetHeight(_standardLabelRect));
	cellHeight = size.height + margin;
	
	// 計算した高さが最小値を下回っていたら無理矢理最小値に補正
	if (cellHeight < _standardCellHeight) {
		return _standardCellHeight;
	}
	
	return cellHeight;
}


// textプロパティのセッターを上書き
// textが設定されると同時に、ラベルにも反映させる
- (void)setText:(NSString *)text
{
	_text = [text copy];
	
	// ラベルに値を反映
	self.textLabel.text = text;
}

@end
```

## MyCellContentView.xib

* UITableViewCellを直接レイアウトしても良いが、今回は「カスタムセルの中身」をUIViewでレイアウトする
* 複数行表示にしたいので、textLabelの行数設定を "0" にする
* textLabelのAutoresizingMaskは、四辺マージン固定・縦横可変にする