前回のつづき

今回はCustom TableViewCellをInterface Builder + Auto Layoutで作る方法です。またセルの高さが可変する場合も含めて説明します。

※ セルの高さ計算は面倒なことだったと思うのですが、Auto Layoutのおかげでその苦しみから解放されると思います。

サンプルコード: https://github.com/stack3/iOSAutoLayoutSamples

サンプルコードを起動しFlexible TableViewCellを選択すると以下の画面が表示されます。

01

Labelが3つ並んでいて、それぞれ同じ文字列が入っています。セルごとに文字列の長さは異なります。つまりセルの高さはそれぞれ異なります。

02

横画面もうまく表示されます。

03

STFlexibleTableViewCell.xib

このファイルでセルのレイアウトをしています。

通常、xibファイルを作るとViewが配置されていますが、この場合はTableViewCellを配置していることに注意です。つまり以下の手順で配置しています。

  • xibファイルを作ったときに最初に配置されているViewを削除する
  • TableViewCellを配置する
  • TableViewCellのAttributes Inspectorを開いてCustom ClassをSTFlexibleTableViewCellにする

09

  • Label(Subview)はContentViewに配置する(自動的にそうなるので特に意識する必要はないです)

08

次はLabelの配置についてです。

04

Labelを縦に3つ並べています。Label間の間隔は8px、LabelとSuperviewとの間隔は20pxになるようにConstraintを設定しています。

一番上の青いLabelのConstraint。

05

真ん中の赤いLabelのConstraint。

06

一番下の黄色いLabelのConstraint。

07

一番下のLabelの下方向のConstraintは点線になっています。これはPriorityが500、つまり通常の1000より低く設定してあるからです。この理由は後で説明します。

STFlexibleTableViewCellクラス

前回までのCustom Viewの場合、クラスのinitWithCoder,initWithFrameで、xibでレイアウトしたViewをロードし、それを自身(self)にaddSubviewしていました。今回のCustom TableViewCellの場合は、xibファイルにTableViewCellそのものを配置しているので、処理が異なります。

UITableView#registerNib:forCellReuseIdentifier:とdequeueReusableCellWithIdentifier:forIndexPath:を使うことで、xib上のTableViewCellのレイアウトと、xibで設定したCustom Class(この場合、STFlexibleTableViewCellクラス)がうまくひもづいた状態で、オブジェクト生成されるからです。

Custom Viewの場合も、そういう仕組があれば良いと思うのですが、探した限りみつかりませんでした。トリッキーな方法は見つかったのですが、Auto Layoutと相性が悪いようです。この件の詳細は以下の記事で書いています。

Custom Viewをxibで作る方法を考察

Custom TableViewCellに話を戻します。

STFlexibleTableViewCellクラスでは、前述した理由からxibからロードする処理を書く必要はありません。ただし前回の記事同様にLabel#preferredMaxLayoutWidthは、セルの幅に応じたサイズに設定する必要があります。

- (void)layoutSubviews
{
    CGRect bounds = self.bounds;
    // 左右の間隔(20 * 2 = 40px)をセルの幅から引いたものを設定
    _label1.preferredMaxLayoutWidth = bounds.size.width - 20*2;
    _label2.preferredMaxLayoutWidth = bounds.size.width - 20*2;
    _label3.preferredMaxLayoutWidth = bounds.size.width - 20*2;
    
    [super layoutSubviews];
}

各Labelにセルのインデックスに応じて長さが異なる文字列を設定するメソッドです。

- (void)setLabelTextsWithIndexPath:(NSIndexPath *)indexPath
{
    NSMutableString *string = [[NSMutableString alloc] initWithCapacity:100];
    for (NSUInteger i = 0; i < indexPath.row + 1; i++) {
        [string appendFormat:@"12345678901234567890123456789012345678901234567890"];
    }
    
    _label1.text = string;
    _label2.text = string;
    _label3.text = string;
}

STFlexibleTableViewCellViewController.storyboard

ここではTableViewをView全体に貼り付け、上下左右の間隔を0pxとするConstraintを設定しているだけです。

10

STFlexibleTableViewCellViewControllerクラス

viewDidLoadは以下のようになっています。

- (void)viewDidLoad
{
    [super viewDidLoad];

    // タイトル設定
    self.title = @"Flexible TableViewCell";

    // xibファイル名を指定しUINibオブジェクトを生成する
    UINib *nib = [UINib nibWithNibName:@"STFlexibleTableViewCell" bundle:nil];
    // UITableView#registerNib:forCellReuseIdentifierで、使用するセルを登録
    [_tableView registerNib:nib forCellReuseIdentifier:_STCellId];
    // これは高さの計算用のセルを登録
    [_tableView registerNib:nib forCellReuseIdentifier:_STCellForHeightId];

    _tableView.dataSource = self;
    _tableView.delegate = self;

    // セルの高さ計算用のオブジェクトをあらかじめ生成して変数に保持しておく
    _cellForHeight = [_tableView dequeueReusableCellWithIdentifier:_STCellForHeightId];
}

_cellForHeightはプロパティとして宣言しています。

@property (strong, nonatomic) STFlexibleTableViewCell *cellForHeight;

UITableViewDataSourceを通じて行数とIndexPathに応じたセルオブジェクトの返却を行います。

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return 3;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    STFlexibleTableViewCell *cell = [_tableView dequeueReusableCellWithIdentifier:_STCellId forIndexPath:indexPath];
    [cell setLabelTextsWithIndexPath:indexPath];
    
    return cell;
}

この辺りは、以下で詳しく説明しているので、わからない人はそちらを参考にしてください。

UITableViewチュートリアル

そして今回の最も重要な箇所、セルの高さの自動計算です。

セルの高さは、UITableViewDelegate#tableView:heightForRowAtIndexPath:で返します。

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    _cellForHeight.frame = _tableView.bounds;
    
    // これでもよいが、上記の方が記述が楽。高さは自動計算するので、ここでは適当で良い。
    // _cellForHeight.frame = CGRectMake(0, 0, _tableView.bounds.size.width, 0);
    
    // indexPathに応じた文字列を設定
    [_cellForHeight setLabelTextsWithIndexPath:indexPath];
    
    // Custom CellのcontentViewを再レイアウトする
    [_cellForHeight.contentView setNeedsLayout];
    [_cellForHeight.contentView layoutIfNeeded];
    // 適切なサイズをAuto Layoutによって自動計算する
    CGSize size = [_cellForHeight.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
    // 自動計算で得られた高さを返す
    return size.height;
}

UIView#systemLayoutSizeFittingSize:を使うと、そのViewのSubviewおよびConstraintから最適な高さを計算して返してくれます。引数にはUILayoutFittingCompressedSizeかUILayoutFittingExpandedSizeを指定するのですが、基本、UILayoutFittingCompressedSizeしか使わないと思います。

注意すべきなのはsystemLayoutSizeFittingSizeは、セルではなく、contentViewから呼ぶことです。セルの方で呼んでも0が返ってきてしまいます。

一番下のLabelの下方向のConstraintのPriorityを低く設定している件

12

最後にこの件について。このPriorityを通常の1000に戻すと一番下のLabelの高さがおかしくなります。

11

systemLayoutSizeFittingSizeが意図したサイズを返さないことが原因です。

各Labelの垂直方向のConstraintが複数存在し、優先度がすべてMAX(1000)になっていると、互いに足を引っ張ってレイアウトがうまくいかなくなるようです。この推測が正確かどうかはわかりませんが、おそらくは一番下のLabelの下方向のConstraint(20px)を保つことが優先されて、Labelの高さが広がってしまうのではと思います。そこでPriorityを500に下げるとうまくいくようになりました。