前回はセルの高さは違えどセルのクラスはSTTweetCellで統一されていて同じクラスでした。しかし場合によっては、セルのクラスが異なるものを1つのTableViewで表示する必要があります。今回はそのケースについて説明します。

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

サンプルコードのクラスの説明

STFlexibleCellHeightViewController2

  • STTweetCell、STImageAndCaptionCellを交互に表示するTableViewを保持する

STTweetCell

ツイートもどきを表示するセル。詳細はその1を参照。

STTweetRowData

セルに表示するツイートのデータ。詳細はその1を参照。

STImageAndCaptionCell

画像とキャプションを下部に表示するセル。今回はSTTweetCellのように新たにsubviewを生成していません。super classであるUITableViewのsubviewプロパティを使っています。

  • imageViewプロパティ – 画像の表示に使用
  • textLabelプロパティ – キャプションの表示に使用

実はlayoutSubviewsでこれらのプロパティのframeを設定すれば任意の位置に配置できるのです。今回はそのようにしています。

STImageAndCaptionRowData

セルに表示する画像とキャプションのデータ

  • image  – 表示する画像(UIImage)
  • caption – 表示するキャプション(NSString)

STImageAndCaptionCellの実装

STTweetCellの時と同じようにlayoutSubview, sizeThatFits:, sizeThatFits:withLayoutを実装します。

/**
 * Override UIView method
 */
- (void)layoutSubviews
{
    [super layoutSubviews];

    CGRect bounds = self.bounds;
    [self sizeThatFits:bounds.size withLayout:YES];
}

/**
 * Override UIView method
 */
- (CGSize)sizeThatFits:(CGSize)size
{
    return [self sizeThatFits:size withLayout:NO];
}

/**
 * @param size Bounds size
 * @param withLayout YES if set frame of subviews.
 */
- (CGSize)sizeThatFits:(CGSize)size withLayout:(BOOL)withLayout
{
    CGRect imageViewFrame;
    imageViewFrame.origin.x = STMargin;
    imageViewFrame.origin.y = STMargin;
    imageViewFrame.size.width = size.width - STMargin*2;
    imageViewFrame.size.height = self.imageView.image.size.height * imageViewFrame.size.width / self.imageView.image.size.width;
    if (withLayout) {
        self.imageView.frame = imageViewFrame;
    }

    CGRect captionLabelFrame;
    captionLabelFrame.origin.x = STMargin;
    captionLabelFrame.origin.y = imageViewFrame.origin.y + imageViewFrame.size.height;
    captionLabelFrame.size.width = size.width - STMargin*2;
    captionLabelFrame.size.height = size.height;
    captionLabelFrame.size = [self.textLabel sizeThatFits:captionLabelFrame.size];
    if (withLayout) {
        self.textLabel.frame = captionLabelFrame;
    }

    size.height = captionLabelFrame.origin.y + captionLabelFrame.size.height + STMargin;
    return size;
}

画像はできるだけ幅いっぱいに表示するようにしています。よってimageViewの高さは可変です。その下にキャプション表示のtextLabelを配置しています。

高さ計算用のSTImageAndCaptionCell

STTweetCellと同じく、高さ計算用のSTImageAndCaptionCellを用意し、STFlexibleCellHeightViewController2がメンバー変数として保持します。

/**
 * These cells are just used to calculate row height. Not display.
 */
__strong STTweetCell *_tweetCellForHeight;
__strong STImageAndCaptionCell *_imageAndCaptionCellForHeight;

viewDidLoadでこれらを生成します。

    _tweetCellForHeight = [[STTweetCell alloc] initWithFrame:CGRectZero];
    _imageAndCaptionCellForHeight = [[STImageAndCaptionCell alloc] initWithFrame:CGRectZero];

UITableViewDataSource/UITableViewDelegateの各メソッドの実装

tableView:cellForRowAtIndexPath:でセルの生成と再利用の仕組みを実装します。その前にデータのクラスについて説明します。実はSTTweetRowDataとSTImageAndCaptionRowDataは、STTableViewRowDataのsubclassにしてあります

/**
 * Base class of TableView row data.
 */
@interface STTableViewRowData : NSObject

/**
 * Return cell identifier. Subclass overrides this method.
 */
+ (NSString *)cellIdentifier;
/**
 * Return cell class. Subclass overrides this method.
 */
+ (Class)cellClass;

@end

cellIdentifier、cellClassはsubclass、つまりSTTweetRowData、STImageAndCaptionRowDataで実装します。

cellIdenfitier

再利用のためのをcell identifierを返す。クラスごとに異なるものを返すことで再利用対象を区別できる。STTweetRowDataでは@”tweetRowData”、STImageAndCaptionRowDataでは@”imageAndCaptionRowData”を返すようにしています。データのクラスが決まればcell identifierも決まるので、クラスメソッドで定義してあります。

cellClass

このデータを表示するセルのクラスを返す。STTweetRowDataは[STTweetCell class]、STImageAndCaptionRowDataは[STImageAndCaptionCell class]を返すようにしています。データのクラスが決まればセルのクラスも決まるので、クラスメソッドで定義してあります。

tableView:cellForRowAtIndexPath:で、これらのメソッドを使って適切なセルの生成や再利用を行っています。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    STTableViewRowData *rowData = [_rows objectAtIndex:indexPath.row];

    NSString *cellIdentifier = [[rowData class] cellIdentifier];
    STTableViewCell *cell = (STTableViewCell *)[_tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (cell == nil) {
        Class cellClass = [[rowData class] cellClass];
        cell = [[cellClass alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
    }

    [cell setupRowData:rowData];

    return cell;
}

[_rows objectAtIndex:indexPath.row]で取り出したrowDataのclassオブジェクト経由でcellIdentifierメソッドを呼び出し、cell identifierを取得します。それをdequeueReusableCellWithIdentifier:に渡せば、STTweetCell、STImageAndCaptionCellどちらか適切なものが返されます。もしまだ再利用できるセルがなければ生成するようにしています。

rowDataのclassオブジェクト経由でcellClassメソッドを呼び出せば、生成すべきセルのclassオブジェクトを取得できます。そのclassオブジェクト経由でallocしてinitWithStyle:reuseIdentiferを呼び出しセルを生成します。

このようにここはデータとセルの具体的なクラスを意識せずに処理できるようになっています。

tableView:heightForRowAtIndexPath:

tableView:heightForRowAtIndexPath:でセルの高さを返すようにします。

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

    STTableViewRowData *row = [_rows objectAtIndex:indexPath.row];
    STTableViewCell *cellForHeight = nil;
    if ([row isKindOfClass:[STTweetRowData class]]) {
        cellForHeight = _tweetCellForHeight;
    } else if ([row isKindOfClass:[STImageAndCaptionRowData class]]) {
        cellForHeight = _imageAndCaptionCellForHeight;
    }
    [cellForHeight setupRowData:row];

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

}

rowのisKindOfClassで実際のクラスを判定し、ローカル変数のcellForHeightに高さ計算で使用するセルオブジェクトを代入します。ちなみにSTTweetCell、STImageAndCaptionCellはSTTableViewCellを継承しています。STTableViewCellはsetupRowDataメソッドを持っています。

setupRowDataでデータを代入した後、sizeThatFits:で高さを計算しています。これでSTTweetRowData、STImageAndCaptionRowDataどちらであれ適切なセルの高さが返されるようになりました。

ちなみに画面の回転にも対応しています。縦から横にすると画像のサイズが引き伸ばされるのがわかるでしょう。それによってSTImageAndCaptionCellの高さが変わることも確認できると思います。

f:id:eimei23:20121210171743p:plain

f:id:eimei23:20121210171748p:plain

layoutSubviewsがうまく呼ばれないとき

UITableViewCellのlayoutSubviewsですが、たいていは然るべきタイミングで呼ばれます。しかし場合によってはセルのframeが変更されても呼ばれないことがあるようです(はっきりと確認できていませんが)。もしそのようなケースに遭遇したらUITableViewのsublassで以下のように実装すると良いと思います。

- (void)setFrame:(CGRect)frame {
    [super setFrame:frame];
    [self setNeedsLayout];
}

以上、高さが異なるセルを持つUITableViewのサンプルコードの解説でした。