前回のつづき。

サンプル: https://github.com/stack3/STFlexibleCellHeightSample

STTweetCellの実装が終わり、セルの高さの計算およびセルのsubview配置はできるようになりました。次はtableView:heightForRowAtIndexPath:で、どのように高さを返すかです。

高さ計算用のセルを1つだけ用意する

前回述べたようにtableView:heightForRowAtIndexPath:は表示範囲外のセルに対しても呼ばれるので、実際に表示するセルとは分離して考えないといけません。

このサンプルでは高さ計算用のセルを別個1つだけ用意して、それをViewControllerに持つようにしています。

@interface STFlexibleCellHeightViewController : UIViewController {
    __strong NSMutableArray *_tweets;
    __weak UITableView *_tableView;
    /** 
     * This cell is just used to calculate height for row. Not display.
     */
    __strong STTweetCell *_cellForHeight;
}

_cellForHeightというメンバー変数がそれです。viewDidLoadで生成します。あとはtableView:heightForRowAtIndexPath:で、_cellForHeightにデータを設定してから高さを返すだけです。

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"tableView:heightForRowAtIndexPath indexPath.row:%d", indexPath.row);

    STTweetRowData *tweet = [_tweets objectAtIndex:indexPath.row];
    [_cellForHeight setupRowData:tweet];

    CGSize size;
    size.width = _tableView.frame.size.width;
    size.height = STMaxCellHeight;
    size = [_cellForHeight sizeThatFits:size];
    return size.height;
}

セルの幅の範囲はtableViewの幅と同じ、高さの範囲はSTMaxCellHeightにしています。STMaxCellHeightは、STDefines.hに2000と定義してあります。UITableViewのAPI Referenceにあるようにセルの高さは最大2009までのようです。

Important Due to an underlying implementation detail, you should not return values greater than 2009.

よってこのように定義し、高さを求める際もそれを上限としています。

とりあえず、これで高さの異なるセルがうまく表示されるはずです。

パフォーマンス上の注意

tableView:heightForRowAtIndexPath:はセルの総数分一度に呼ばれることがあるので、実装次第ではパフォーマンスに影響します。画面回転に対応させている時、TableViewのサイズも変わるのでtableView:heightForRowAtIndexPath:が全てのセルに対して呼ばれます。パフォーマンスが悪いと画面を回転してから数秒して実際の表示が回転するといった事が起きます。

一度求めた高さはindexPathと高さの組みわせでNSMutableDictionaryなどにキャッシュしておくとよいでしょう。

データが変わり高さもそれに応じて変わる場合

ユーザーが編集するなどしてセルの表示内容が変わって、高さが変わるというケースもありえます。データの内容だけ変更してもセルの高さは自動で変わってくれません。明示的に高さ変更の指示を出す必要があります。

データを変更した後、セルの高さを再計算させる方法は、

  • UITableView#reloadDataを呼ぶ
  • UITableView#beginUpdates/endUpdatesを呼ぶ

の2パターンです。reloadDataを呼ぶと再びtableView:heightForRowAtIndexPath:とtableView:cellForRowAtIndexPath:が呼ばれて、高さの計算とセルへのデータ設定が再度行われます。通常はreloadDataで良いと思いますが、セルの選択状態が解除されるので注意が必要です。

beginUpdates/endUpdatesはアニメーションしてセルの高さが変わります。ただしtableView:heightForRowAtIndexPath:しか呼ばれないので、データが変更されたセルが表示中なら明示的にデータの再設定が必要です。

本サンプルではセルをタップするとSTTweetRowDataのstatusを編集する画面に遷移します。編集後戻るを押すと、reloadDataでセルの再表示を行なっています。

- (void)editTextViewController:(STEditTextViewController *)sender didEditText:(NSString *)text
{
    NSArray *selectedIndexPaths = [_tableView indexPathsForSelectedRows];
    if (selectedIndexPaths.count > 0) {
        NSIndexPath *indexPath = [selectedIndexPaths objectAtIndex:0];
        STTweetRowData *tweet = [_tweets objectAtIndex:indexPath.row];
        tweet.status = text;

        if (YES) {
            [_tableView reloadData];
        } else {
            // Notice. Return nil if the cell was not visible.
            STTweetCell *cell = (STTweetCell *)[_tableView cellForRowAtIndexPath:indexPath];
            if (cell) {
                [cell setupRowData:tweet];
                [_tableView beginUpdates];
                [_tableView endUpdates];
            }
        }
    }
}

if (YES)の部分をNOにするとbeginUpdates/endUpdatesを使う方に切り替わります。Debuggerでbreakpointを貼るとtableView:heightForRowAtIndexPath:とtableView:cellForRowAtIndexPath:両方呼ばれるかtableView:heightForRowAtIndexPath:のみか確認できます。

次回は、subview構成が異なるセルを混合して表示する場合について説明します。

その3へつづく。