ちょっと話題の記事

[iOS] Auto Layout + Storyboard で高さ可変のUITableViewCellを作成する

2014.05.07

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

高さ可変のテーブルビューセルを作成する方法はいくつかありますが、今回は「Storyboard」上で「Auto Layout」を設定することで実現してみます。環境は以下の通りです。

  • Xcode 5.1.1
  • SDK 7.1
  • Development Target 7.0

完成図

how-to-make-adjustable-cell_01 ↑こんな感じに仕上がります。

「Master-Detail Application」テンプレートに手を加えていきます。「Master-Detail Application」は画面上部のプラスボタンを押すと、現在時刻を表示するセルが増えるサンプルですが、セルのラベルを2つに増やし、高さが可変のラベル(ランダムな文字列を表示)と高さ固定のラベル(時刻を表示)が表示されるようにします。また、ラベルの高さに合わせてセルの高さも変わるようにします。

実装

プロジェクト作成

XcodeのFileメニューから New → Project と辿り、「Master-Detail Application」テンプレートからプロジェクトを作成してください。

セルの基本設定

how-to-make-adjustable-cell_00 Main.storyboardを開いて、「Master View Controller」シーン内のTableViewCellを選択し、attributes inspectorを表示させます。

how-to-make-adjustable-cell_01 セルのStyle項目を「Custom」に変更します。

how-to-make-adjustable-cell_02 次にセルの高さを100ぐらいに変更し、セルの中にラベルを2つ追加します。

how-to-make-adjustable-cell_03 区別がつきやすいようにデフォルトのテキストを「mainLabel」及び「subLabel」に変更します。(以後、これらのラベルをそれぞれmainLabel、subLabelと呼ぶことにします) mainLabelのフォントサイズを17にします。また、subLabelのフォントサイズを14にして、subLabelのフォントカラーをLight Gray Colorに変更します。 ラベルの幅などは適当な大きさに変更して構いません(後で設定するAuto layoutによって調整されます。)

how-to-make-adjustable-cell_14 また、「mainLabel」のLines項目を0に変更します。(subLabelの方は変更しません)

セルのAuto Layout設定

Auto Layoutの設定を行っていきます。 how-to-make-adjustable-cell_04 上図のような感じでContentViewとLabel間のConstraintsを設定していきます。

how-to-make-adjustable-cell_05 mainLabelを選択した状態で、Pinメニューをクリックしてください。

how-to-make-adjustable-cell_06 mainLabelのconstantの値を入力していきます。テキストボックスに数値を入力し、隣接する赤い棒状の部分をクリックします。(赤い棒状の部分をクリックすると、棒の線が点線→実線に変化します)

how-to-make-adjustable-cell_07 上下左右4つ分の値を上記画像の通りに入力します。上・左・右はセルのContentsViewのそれぞれの端とmainLabelとの間、下はsubLabelとmainLabelとの間に対する設定になります。 数値の入力が終わったら「Add 4 Constraints」をクリックします。これでconstantが追加されます。

how-to-make-adjustable-cell_08 追加されました!

how-to-make-adjustable-cell_12 subLabelの場合も同様の手順で入力します。ただし、subLabelの場合はheightも設定します。 上記画像の通りに入力し、「Add 5 Constraints」をクリックしてください。

how-to-make-adjustable-cell_18 追加できました... しかし、Warningが表示されてしまってます。黄色いボタンをクリックして、詳細を確認しましょう。

how-to-make-adjustable-cell_20 size Inspectorで設定してある"Frame"と"制約"との間に矛盾が発生してしまっています。三角のアイコンをクリックするとメニューが出てきます。

how-to-make-adjustable-cell_21 Update Frameを選択し、Fix Misplacementをクリックします。(mainLabelの分とsubLabelの分をそれぞれ修正します。) これで、Frameが制約にあわせたものになりました。

セルのクラスファイル作成/接続

how-to-make-adjustable-cell_15 UITableViewCellのサブクラスを作成します。CustomCellという名前にしました。

how-to-make-adjustable-cell_16 Main.storyboardの編集に戻ります。MasterViewControllerシーンのTableViewCellをクリックし、Identity Inspectorを選択します。「Class」項目にCustomCellと入力します。(先ほど作ったクラスです)

how-to-make-adjustable-cell_17 また、mainLabel/subLabelをCustomCellクラスのプロパティとして追加します。

MasterViewControllerの修正

MasterViewControllerクラスの実装ファイルを修正していきます。

importとインスタンス変数の追加

#import "CustomCell.h"	// 追加

@interface MasterViewController () {
    NSMutableArray *_objects;
    NSArray *_textArray;	// 追加
    CustomCell *_stubCell;	// 追加
}

_textArrayにはセルのラベルに表示する文字列を格納します。_stubCellはセルの高さを計算するときに使用します。

ViewDidLoadメソッドの修正

- (void)viewDidLoad
{
    [super viewDidLoad];
	// Do any additional setup after loading the view, typically from a nib.
    self.navigationItem.leftBarButtonItem = self.editButtonItem;

    UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(insertNewObject:)];
    self.navigationItem.rightBarButtonItem = addButton;
    
    _stubCell = [self.tableView dequeueReusableCellWithIdentifier:@"Cell"];	// 追加
    
    // 追加
    // 文字列の配列の作成
    _textArray = @[
                   @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sodales diam sed turpis mattis dictum. In laoreet porta eleifend. Ut eu nibh sit amet est iaculis faucibus.",
                   @"initWithBitmapDataPlanes:pixelsWide:pixelsHigh:bitsPerSample:samplesPerPixel:hasAlpha:isPlanar:colorSpaceName:bitmapFormat:bytesPerRow:bitsPerPixel:",
                   @"祇辻飴葛蛸鯖鰯噌庖箸",
                   @"Nam in vehicula mi.",
                   @"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.",
                   @"あのイーハトーヴォの\nすきとおった風、\n夏でも底に冷たさをもつ青いそら、\nうつくしい森で飾られたモーリオ市、\n郊外のぎらぎらひかる草の波。",
                   ];
}

_stubCell/_textArrayの作成コードを追加します。"dequeueReusableCellWithIdentifier:"メソッドで指定するidentifierはStoryboard上で指定しているものと同じです。

insertNewObject:メソッドの修正

- (void)insertNewObject:(id)sender
{
    if (!_objects) {
        _objects = [[NSMutableArray alloc] init];
    }
    
    // 追加
    // データ作成
    int dataIndex = arc4random() % _textArray.count;
    NSString *string = _textArray[dataIndex];
    NSDate *date = [NSDate date];
    NSDictionary *dataDictionary = @{@"string": string, @"date":date};
    
    // データ挿入
    [_objects insertObject:dataDictionary atIndex:0];	// 修正
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
    [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}

テンプレートから作成されるinsertNewObject:メソッドを修正します。このメソッドは画面のプラスボタンをタップした際に呼ばれます。「_textArrayの中から無作為に選んだ文字列」と「現在時刻」を格納したDictionaryを_objectsプロパティに格納するように修正します。

configureCell:atIndexPath:メソッドの追加

- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
    CustomCell *customCell = (CustomCell *)cell;
    
    // メインラベルに文字列を設定
    NSDictionary *dataDictionary = _objects[indexPath.row];
    customCell.mainLabel.text = dataDictionary[@"string"];
    
    // サブラベルに文字列を設定
    NSDate *date = dataDictionary[@"date"];
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.dateFormat = @"yyyy年MM月dd日 HH時mm分ss秒";
    customCell.subLabel.text = [dateFormatter stringFromDate:date];
}

セルの構成を行うメソッドを追加します。このメソッドは、"tableView:heightForRowAtIndexPath:"メソッド及び"tableView:cellForRowAtIndexPath:"メソッド内で使用します。

"tableView:heightForRowAtIndexPath:"メソッドと"tableView:estimatedHeightForRowAtIndexPath:"メソッドの追加

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 計測用のプロパティ"_stubCell"を使って高さを計算する
    [self configureCell:_stubCell atIndexPath:indexPath];
    [_stubCell layoutSubviews];
    CGFloat height = [_stubCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
    
    return height + 1;
}

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return 40.0;
}

"tableView:heightForRowAtIndexPath:"メソッドでは計測用に用意した_stubCellを使ってセルに必要な高さを計算します。(強制的にlayoutSubviewsを呼んで必要な高さを計算します。)また、"tableView:estimatedHeightForRowAtIndexPath:"メソッドはiOS 7から追加されたもので、セルの大体の高さを渡します。

tableView:cellForRowAtIndexPath:メソッドの修正

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];

    [self configureCell:cell atIndexPath:indexPath];	// 追加
    	
    return cell;
}

キューから取り出したセルをconfigureCell:atIndexPath:メソッドに渡す処理を追加します。 これで実装は完了です。

まとめ

今回は、Auto Layout + Storyboard で高さ可変のセルを作成してみました。「計測用のセルを用意して各行に必要な高さを計算する」というやり方なので、行数が大量の場合はパフォーマンス的に厳しいかもしれません。「カレンダーのイベント詳細画面」などの数行程度のテーブルビューであれば使えそうかなと思います。

なお、今回作成したプロジェクトはGitHubで公開しています。

参考ページ