UITableView で自動レイアウトを使用して動的なセルレイアウトと可変行の高さを実現する 質問する

UITableView で自動レイアウトを使用して動的なセルレイアウトと可変行の高さを実現する 質問する

テーブル ビュー内の 内で自動レイアウトを使用して、スムーズなスクロール パフォーマンスを維持しながら、各セルのコンテンツとサブビューが行の高さを (それ自体で/自動的に) 決定するようにするにはどうUITableViewCellすればよいですか?

ベストアンサー1

TL;DR:読むのが嫌ですか? GitHub のサンプル プロジェクトに直接ジャンプしてください:

概念の説明

以下の最初の 2 つの手順は、開発対象の iOS バージョンに関係なく適用できます。

1. 制約の設定と追加

サブクラスで制約を追加して、セルのサブビューの端がセルのコンテンツビューUITableViewCellの端に固定されるようにします(最も重要なのは、上端と下端に固定することです)。注: サブビューをセル自体に固定しないでください。セルの のみに固定してください。各サブビューの垂直方向のコンテンツ圧縮抵抗とコンテンツ ハグ制約が、追加した優先度の高い制約によって上書きされないようにすることで、これらのサブビューの固有のコンテンツ サイズによってテーブル ビュー セルのコンテンツ ビューの高さが決まります。(contentViewえ?ここをクリック。

覚えておいてください。セルのサブビューをセルのコンテンツ ビューに垂直に接続して、「圧力をかける」ことで、コンテンツ ビューがサブビューに合わせて拡大されるようにするという考え方です。いくつかのサブビューを持つサンプル セルを使用して、制約の一部 (すべてではありません!)がどのようになるかを視覚的に示します。

テーブル ビュー セルの制約の例の図。

上記の例のセルの複数行の本文ラベルにテキストが追加されると、テキストに合うようにラベルを垂直方向に拡大する必要があり、実質的にセルの高さが大きくなることが想像できます。(もちろん、これを正しく機能させるには、制約を正しく設定する必要があります。)

制約を正しく設定することは、Auto Layout で動的なセルの高さを機能させる上で間違いなく最も難しく、最も重要な部分です。ここでミスをすると、他のすべてが機能しなくなる可能性があります。そのため、時間をかけてください。コードで制約を設定することをお勧めします。どの制約がどこに追加されるかを正確に把握でき、問題が発生した場合のデバッグがはるかに簡単になるためです。コードで制約を追加することは、レイアウト アンカーを使用する Interface Builder や、GitHub で利用できる優れたオープン ソース API の 1 つと同じくらい簡単で、はるかに強力です。

  • updateConstraintsコードで制約を追加する場合は、 UITableViewCell サブクラスのメソッド内からこれを 1 回実行する必要があります。 はupdateConstraints複数回呼び出される可能性があるため、同じ制約を複数回追加しないようにするには、制約を追加するコードをupdateConstraintsなどのブール型プロパティのチェックでラップしてくださいdidSetupConstraints(制約を追加するコードを 1 回実行した後、これを YES に設定します)。一方、既存の制約を更新するコード (一部の制約のプロパティを調整するなど) がある場合は、これをのチェック内かつチェックの外側にconstant配置して、メソッドが呼び出されるたびに実行されるようにします。updateConstraintsdidSetupConstraints

2. 一意のテーブルビューセル再利用識別子を決定する

セル内の一意の制約セットごとに、一意のセル再利用識別子を使用します。つまり、セルに複数の一意のレイアウトがある場合は、各一意のレイアウトに独自の再利用識別子を割り当てる必要があります。(新しい再利用識別子を使用する必要があることを示す良いヒントは、セル バリアントに異なる数のサブビューがある場合、またはサブビューが異なる方法で配置されている場合です。)

たとえば、各セルに電子メール メッセージを表示する場合、件名のみのメッセージ、件名と本文のあるメッセージ、件名と写真の添付ファイルのあるメッセージ、件名、本文、写真の添付ファイルのあるメッセージという 4 つの固有のレイアウトが存在する可能性があります。各レイアウトには、レイアウトを実現するために必要な制約がまったく異なるため、セルが初期化され、これらのセル タイプのいずれかに制約が追加されると、セルはそのセル タイプに固有の一意の再利用識別子を取得する必要があります。つまり、再利用のためにセルをキューから取り出すと、制約はすでに追加されており、そのセル タイプで使用できる状態になります。

固有のコンテンツ サイズの違いにより、同じ制約 (タイプ) を持つセルでも高さが異なる場合があります。コンテンツのサイズが異なるために、根本的に異なるレイアウト (異なる制約) と、計算されたビュー フレーム (同一の制約から解決) が異なることを混同しないでください。

  • まったく異なる制約セットを持つセルを同じ再利用プールに追加しないでください (つまり、同じ再利用識別子を使用します)。その後、各デキュー後に古い制約を削除して、新しい制約を最初から設定しようとしないでください。内部の自動レイアウト エンジンは、制約の大規模な変更を処理するように設計されていないため、パフォーマンスに大きな問題が発生します。

iOS 8 の場合 - セルの自動サイズ調整

3. 行の高さの推定を有効にする

テーブルビューのセルの自動サイズ調整を有効にするには、テーブルビューの rowHeight プロパティを UITableViewAutomaticDimension に設定する必要があります。また、estimatedRowHeight プロパティに値を割り当てる必要があります。これらのプロパティの両方が設定されると、システムは Auto Layout を使用して行の実際の高さを計算します。

りんご:自動サイズ調整テーブルビューセルの操作

iOS 8 では、iOS 8 より前はユーザーが実装しなければならなかった作業の多くを Apple が内部化しました。セルの自動サイズ調整メカニズムを機能させるには、まずrowHeightテーブル ビューの プロパティを定数に設定する必要があります。次に、テーブル ビューのプロパティを 0 以外の値にUITableView.automaticDimension設定して、行の高さの推定を有効にするだけです。次に例を示します。estimatedRowHeight

self.tableView.rowHeight = UITableView.automaticDimension;
self.tableView.estimatedRowHeight = 44.0; // set to whatever your "average" cell height is

これは、まだ画面上に表示されていないセルの行の高さの一時的な推定値/プレースホルダーをテーブル ビューに提供します。次に、これらのセルが画面上でスクロールされるときに、実際の行の高さが計算されます。各行の実際の高さを決定するために、テーブル ビューは、contentViewコンテンツ ビューの既知の固定幅 (テーブル ビューの幅に基づき、セクション インデックスやアクセサリ ビューなどの追加要素を除いたもの) と、セルのコンテンツ ビューとサブビューに追加した自動レイアウト制約に基づいて、各セルに必要な高さを自動的に確認します。この実際のセルの高さが決定されると、行の古い推定高さが新しい実際の高さで更新されます (必要に応じて、テーブル ビューの contentSize/contentOffset の調整が行われます)。

一般的に言えば、提供する推定値は非常に正確である必要はありません。これは、テーブル ビューのスクロール インジケーターのサイズを正しく設定するためにのみ使用され、画面上でセルをスクロールするときに、テーブル ビューはスクロール インジケーターの誤った推定値を適切に調整します。estimatedRowHeightテーブル ビュー (viewDidLoadまたは同様のビュー) のプロパティを、行の「平均」高さである定数値に設定する必要があります。行の高さに極端なばらつきがあり (たとえば、桁違いに異なる)、スクロールするとスクロール インジケーターが「ジャンプ」することに気付いた場合にのみ、tableView:estimatedHeightForRowAtIndexPath:各行のより正確な推定値を返すために必要な最小限の計算を実行するように実装する必要があります。

iOS 7 のサポート (自動セルサイズ設定を自分で実装する)

3. レイアウトパスを実行してセルの高さを取得する

まず、テーブル ビュー セルのオフスクリーン インスタンスをインスタンス化します。これは、再利用識別子ごとに 1 つのインスタンスtableView:cellForRowAtIndexPath:で、高さの計算にのみ使用されます。(オフスクリーンとは、セル参照がビュー コントローラのプロパティ/ivar に格納され、テーブル ビューが実際に画面上にレンダリングするために返されることはないことを意味します)。次に、セルは、テーブル ビューに表示される場合に保持される正確なコンテンツ (テキスト、画像など) で構成する必要があります。

次に、セルにサブビューをすぐにレイアウトするように強制し、 の メソッドを使用して、セルsystemLayoutSizeFittingSize:必要な高さを調べます。 を使用して、セルのすべてのコンテンツに適合するために必要な最小サイズを取得します。その後、デリゲート メソッドから高さを返すことができますUITableViewCellcontentViewUILayoutFittingCompressedSizetableView:heightForRowAtIndexPath:

4. 推定行の高さを使用する

テーブル ビューに数十行以上ある場合、自動レイアウト制約の解決を行うと、テーブル ビューを最初に読み込むときにメイン スレッドがすぐに停止することがわかります。これは、最初の読み込み時tableView:heightForRowAtIndexPath:に各行に対して呼び出されるためです (スクロール インジケーターのサイズを計算するため)。

iOS 7 以降では、estimatedRowHeightテーブル ビューで プロパティを使用できます (絶対に使用する必要があります)。これにより、まだ画面上に表示されていないセルの行の高さの一時的な推定値/プレースホルダーがテーブル ビューに提供されます。その後、これらのセルが画面上でスクロールされるときに、実際の行の高さが計算され ( を呼び出すことによってtableView:heightForRowAtIndexPath:)、推定された高さが実際の高さに更新されます。

一般的に言えば、提供する推定値は非常に正確である必要はありません。これは、テーブル ビューのスクロール インジケーターのサイズを正しく設定するためにのみ使用され、画面上でセルをスクロールするときに、テーブル ビューはスクロール インジケーターの誤った推定値を適切に調整します。estimatedRowHeightテーブル ビュー (viewDidLoadまたは同様のビュー) のプロパティを、行の「平均」高さである定数値に設定する必要があります。行の高さに極端なばらつきがあり (たとえば、桁違いに異なる)、スクロールするとスクロール インジケーターが「ジャンプ」することに気付いた場合にのみ、tableView:estimatedHeightForRowAtIndexPath:各行のより正確な推定値を返すために必要な最小限の計算を実行するように実装する必要があります。

5. (必要な場合) 行の高さのキャッシュを追加する

上記のすべてを実行しても、 で制約を解決するときにパフォーマンスが許容できないほど遅い場合はtableView:heightForRowAtIndexPath:、残念ながらセルの高さのキャッシュを実装する必要があります。(これは、Apple のエンジニアが提案したアプローチです。) 基本的な考え方は、最初に Autolayout エンジンに制約を解決させ、そのセルの計算された高さをキャッシュして、そのセルの高さに対する今後のすべての要求にキャッシュされた値を使用することです。もちろん、秘訣は、セルの高さを変更する可能性のある何かが発生したときに、セルのキャッシュされた高さをクリアすることです。主に、セルのコンテンツが変更されたとき、またはその他の重要なイベントが発生したとき (ユーザーが Dynamic Type のテキスト サイズ スライダーを調整するなど) がこれに該当します。

iOS 7 汎用サンプルコード (興味深いコメントが多数)

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Determine which reuse identifier should be used for the cell at this 
    // index path, depending on the particular layout required (you may have
    // just one, or may have many).
    NSString *reuseIdentifier = ...;

    // Dequeue a cell for the reuse identifier.
    // Note that this method will init and return a new cell if there isn't
    // one available in the reuse pool, so either way after this line of 
    // code you will have a cell with the correct constraints ready to go.
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier];
         
    // Configure the cell with content for the given indexPath, for example:
    // cell.textLabel.text = someTextForThisCell;
    // ...
    
    // Make sure the constraints have been set up for this cell, since it 
    // may have just been created from scratch. Use the following lines, 
    // assuming you are setting up constraints from within the cell's 
    // updateConstraints method:
    [cell setNeedsUpdateConstraints];
    [cell updateConstraintsIfNeeded];

    // If you are using multi-line UILabels, don't forget that the 
    // preferredMaxLayoutWidth needs to be set correctly. Do it at this 
    // point if you are NOT doing it within the UITableViewCell subclass 
    // -[layoutSubviews] method. For example: 
    // cell.multiLineLabel.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds);
    
    return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Determine which reuse identifier should be used for the cell at this 
    // index path.
    NSString *reuseIdentifier = ...;

    // Use a dictionary of offscreen cells to get a cell for the reuse 
    // identifier, creating a cell and storing it in the dictionary if one 
    // hasn't already been added for the reuse identifier. WARNING: Don't 
    // call the table view's dequeueReusableCellWithIdentifier: method here 
    // because this will result in a memory leak as the cell is created but 
    // never returned from the tableView:cellForRowAtIndexPath: method!
    UITableViewCell *cell = [self.offscreenCells objectForKey:reuseIdentifier];
    if (!cell) {
        cell = [[YourTableViewCellClass alloc] init];
        [self.offscreenCells setObject:cell forKey:reuseIdentifier];
    }
    
    // Configure the cell with content for the given indexPath, for example:
    // cell.textLabel.text = someTextForThisCell;
    // ...
    
    // Make sure the constraints have been set up for this cell, since it 
    // may have just been created from scratch. Use the following lines, 
    // assuming you are setting up constraints from within the cell's 
    // updateConstraints method:
    [cell setNeedsUpdateConstraints];
    [cell updateConstraintsIfNeeded];

    // Set the width of the cell to match the width of the table view. This
    // is important so that we'll get the correct cell height for different
    // table view widths if the cell's height depends on its width (due to 
    // multi-line UILabels word wrapping, etc). We don't need to do this 
    // above in -[tableView:cellForRowAtIndexPath] because it happens 
    // automatically when the cell is used in the table view. Also note, 
    // the final width of the cell may not be the width of the table view in
    // some cases, for example when a section index is displayed along 
    // the right side of the table view. You must account for the reduced 
    // cell width.
    cell.bounds = CGRectMake(0.0, 0.0, CGRectGetWidth(tableView.bounds), CGRectGetHeight(cell.bounds));

    // Do the layout pass on the cell, which will calculate the frames for 
    // all the views based on the constraints. (Note that you must set the 
    // preferredMaxLayoutWidth on multiline UILabels inside the 
    // -[layoutSubviews] method of the UITableViewCell subclass, or do it 
    // manually at this point before the below 2 lines!)
    [cell setNeedsLayout];
    [cell layoutIfNeeded];

    // Get the actual height required for the cell's contentView
    CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;

    // Add an extra point to the height to account for the cell separator, 
    // which is added between the bottom of the cell's contentView and the 
    // bottom of the table view cell.
    height += 1.0;

    return height;
}

// NOTE: Set the table view's estimatedRowHeight property instead of 
// implementing the below method, UNLESS you have extreme variability in 
// your row heights and you notice the scroll indicator "jumping" 
// as you scroll.
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Do the minimal calculations required to be able to return an 
    // estimated row height that's within an order of magnitude of the 
    // actual height. For example:
    if ([self isTallCellAtIndexPath:indexPath]) {
        return 350.0;
    } else {
        return 40.0;
    }
}

サンプルプロジェクト

これらのプロジェクトは、テーブル ビュー セルに UILabels の動的なコンテンツが含まれているため、行の高さが変化するテーブル ビューの完全な動作例です。

Xamarin (C#/.NET)

Xamarinを使っている場合は、こちらをチェックしてくださいサンプルプロジェクトまとめるケント・ブーガート

おすすめ記事