ios - uitableviewcell - xcode tableview cell




在UITableView中使用自動佈局來獲得動態單元格佈局和可變行高 (17)

一個可變高度UITableViewCell的Swift示例

更新了Swift 3

William Hu的Swift答案很好,但是它幫助我在學習第一次做某些事情時有一些簡單而詳細的步驟。 下面的例子是我的測試項目,同時學習製作具有可變單元高度的UITableView 。 我基於這個基本的Swift的UITableView示例 。

完成的項目應該如下所示:

創建一個新項目

它可以只是一個單一視圖應用程序。

添加代碼

將一個新的Swift文件添加到您的項目中。 將其命名為MyCustomCell。 此課程將為您添加到故事板中單元格的視圖設置分支。 在這個基本的例子中,我們將在每個單元格中只有一個標籤。

import UIKit
class MyCustomCell: UITableViewCell {
    @IBOutlet weak var myCellLabel: UILabel!
}

我們稍後會連接這個插座。

打開ViewController.swift並確保你有以下內容:

import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    // These strings will be the data for the table view cells
    let animals: [String] = [
        "Ten horses:  horse horse horse horse horse horse horse horse horse horse ",
        "Three cows:  cow, cow, cow",
        "One camel:  camel",
        "Ninety-nine sheep:  sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep baaaa sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep",
        "Thirty goats:  goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat "]

    // Don't forget to enter this in IB also
    let cellReuseIdentifier = "cell"

    @IBOutlet var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // delegate and data source
        tableView.delegate = self
        tableView.dataSource = self

        // Along with auto layout, these are the keys for enabling variable cell height
        tableView.estimatedRowHeight = 44.0
        tableView.rowHeight = UITableViewAutomaticDimension
    }

    // number of rows in table view
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.animals.count
    }

    // create a cell for each table view row
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell:MyCustomCell = self.tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as! MyCustomCell
        cell.myCellLabel.text = self.animals[indexPath.row]
        return cell
    }

    // method to run when table view cell is tapped
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("You tapped cell number \(indexPath.row).")
    }
}

重要的提示:

  • 以下兩行代碼(以及自動佈局)使可變單元高度成為可能:

    tableView.estimatedRowHeight = 44.0
    tableView.rowHeight = UITableViewAutomaticDimension
    

設置故事板

添加一個表視圖到您的視圖控制器,並使用自動佈局將其固定到四邊。 然後將一個Table View Cell拖動到Table View上。 在原型單元上拖動一個標籤。 使用自動佈局將標籤固定到表格視圖單元格的內容視圖的四個邊緣。

重要的提示:

  • 自動佈局與我上面提到的重要的兩行代碼一起工作。 如果你不使用自動佈局,它不會工作。

其他IB設置

自定義類名和標識符

選擇Table View Cell並將自定義類設置為MyCustomCell (我們添加的Swift文件中類的名稱)。 還要將標識符設置為cell (與上面代碼中用於cellReuseIdentifier字符串相同)。

標籤的零線

在標籤中將行數設置為0 。 這意味著多行,並允許標籤根據其內容調整其大小。

連接插座

  • 控制從故事板中的表視圖拖動到ViewController代碼中的tableView變量。
  • 對您的Prototype單元中的Label執行myCellLabel類中myCellLabel變量的相同操作。

成品

你現在應該可以運行你的項目並獲得可變高度的單元格。

筆記

  • 此示例僅適用於iOS 8及之後的版本。 如果您仍然需要支持iOS 7,那麼這對您而言不起作用。
  • 您未來項目中的自定義單元可能不止一個標籤。 確保你將所有東西都固定好,以便自動佈局可以確定使用的正確高度。 您可能還必須使用垂直抗壓縮和擁抱。 有關更多信息,請參閱這篇文章
  • 如果您不固定前導和尾隨(左側和右側)邊緣,則可能還需要設置標籤的preferredMaxLayoutWidth以便知道何時進行換行。 例如,如果您已經在上面的項目的標籤中添加了一個Center Horizo​​ntally約束,而不是固定前緣和後緣,那麼您需要將此行添加到tableView:cellForRowAtIndexPath方法中:

     cell.myCellLabel.preferredMaxLayoutWidth = tableView.bounds.width
    

也可以看看

如何在表格視圖中使用UITableViewCell的自動佈局來讓每個單元格的內容和子視圖確定行高度(本身/自動),同時保持平滑的滾動性能?


我將@ smileyborg的iOS7解決方案包裝在一個類別中

我決定用@smileyborg把這個聰明的解決方案包裝成一個UICollectionViewCell+AutoLayoutDynamicHeightCalculation類別。

該類別還解決了@ wildmonkey的答案(從nib和systemLayoutSizeFittingSize:加載單元格systemLayoutSizeFittingSize:返回CGRectZero )中概述的問題。

它沒有考慮到任何緩存,但現在適合我的需求。 隨意複製,粘貼和破解它。

UICollectionViewCell + AutoLayoutDynamicHeightCalculation.h

#import <UIKit/UIKit.h>

typedef void (^UICollectionViewCellAutoLayoutRenderBlock)(void);

/**
 *  A category on UICollectionViewCell to aid calculating dynamic heights based on AutoLayout contraints.
 *
 *  Many thanks to @smileyborg and @wildmonkey
 *
 *  @see .com/questions/18746929/using-auto-layout-in-uitableview-for-dynamic-cell-layouts-variable-row-heights
 */
@interface UICollectionViewCell (AutoLayoutDynamicHeightCalculation)

/**
 *  Grab an instance of the receiving type to use in order to calculate AutoLayout contraint driven dynamic height. The method pulls the cell from a nib file and moves any Interface Builder defined contrainsts to the content view.
 *
 *  @param name Name of the nib file.
 *
 *  @return collection view cell for using to calculate content based height
 */
+ (instancetype)heightCalculationCellFromNibWithName:(NSString *)name;

/**
 *  Returns the height of the receiver after rendering with your model data and applying an AutoLayout pass
 *
 *  @param block Render the model data to your UI elements in this block
 *
 *  @return Calculated constraint derived height
 */
- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block collectionViewWidth:(CGFloat)width;

/**
 *  Directly calls `heightAfterAutoLayoutPassAndRenderingWithBlock:collectionViewWidth` assuming a collection view width spanning the [UIScreen mainScreen] bounds
 */
- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block;

@end

UICollectionViewCell + AutoLayoutDynamicHeightCalculation.m

#import "UICollectionViewCell+AutoLayout.h"

@implementation UICollectionViewCell (AutoLayout)

#pragma mark Dummy Cell Generator

+ (instancetype)heightCalculationCellFromNibWithName:(NSString *)name
{
    UICollectionViewCell *heightCalculationCell = [[[NSBundle mainBundle] loadNibNamed:name owner:self options:nil] lastObject];
    [heightCalculationCell moveInterfaceBuilderLayoutConstraintsToContentView];
    return heightCalculationCell;
}

#pragma mark Moving Constraints

- (void)moveInterfaceBuilderLayoutConstraintsToContentView
{
    [self.constraints enumerateObjectsUsingBlock:^(NSLayoutConstraint *constraint, NSUInteger idx, BOOL *stop) {
        [self removeConstraint:constraint];
        id firstItem = constraint.firstItem == self ? self.contentView : constraint.firstItem;
        id secondItem = constraint.secondItem == self ? self.contentView : constraint.secondItem;
        [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:firstItem
                                                                     attribute:constraint.firstAttribute
                                                                     relatedBy:constraint.relation
                                                                        toItem:secondItem
                                                                     attribute:constraint.secondAttribute
                                                                    multiplier:constraint.multiplier
                                                                      constant:constraint.constant]];
    }];
}

#pragma mark Height

- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block
{
    return [self heightAfterAutoLayoutPassAndRenderingWithBlock:block
                                            collectionViewWidth:CGRectGetWidth([[UIScreen mainScreen] bounds])];
}

- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block collectionViewWidth:(CGFloat)width
{
    NSParameterAssert(block);

    block();

    [self setNeedsUpdateConstraints];
    [self updateConstraintsIfNeeded];

    self.bounds = CGRectMake(0.0f, 0.0f, width, CGRectGetHeight(self.bounds));

    [self setNeedsLayout];
    [self layoutIfNeeded];

    CGSize calculatedSize = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];

    return calculatedSize.height;

}

@end

用法示例:

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    MYSweetCell *cell = [MYSweetCell heightCalculationCellFromNibWithName:NSStringFromClass([MYSweetCell class])];
    CGFloat height = [cell heightAfterAutoLayoutPassAndRenderingWithBlock:^{
        [(id<MYSweetCellRenderProtocol>)cell renderWithModel:someModel];
    }];
    return CGSizeMake(CGRectGetWidth(self.collectionView.bounds), height);
}

值得慶幸的是,我們不必在iOS8中做這個爵士樂,但現在就是這樣!


Dynamic Table View Cell Height and Auto Layout

A good way to solve the problem with storyboard Auto Layout:

- (CGFloat)heightForImageCellAtIndexPath:(NSIndexPath *)indexPath {
  static RWImageCell *sizingCell = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    sizingCell = [self.tableView dequeueReusableCellWithIdentifier:RWImageCellIdentifier];
  });

  [sizingCell setNeedsLayout];
  [sizingCell layoutIfNeeded];

  CGSize size = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
  return size.height;
}

TL; DR:不喜歡讀書? 直接跳到GitHub上的示例項目:

概念描述

無論您正在開發哪個iOS版本,以下前兩個步驟都適用。

1.設置和添加約束

在你的UITableViewCell子類中,添加約束,以便單元格的子視圖將其邊緣固定到單元格的contentView邊緣(最重要的是頂部和底部邊緣)。 注意:不要將子視圖固定到單元格本身; 只對單元格的contentView 通過確保每個子視圖的垂直維度中的內容壓縮抵抗內容擁抱約束不會被您添加的高優先級約束所覆蓋,讓這些子視圖的內在內容大小驅動表視圖單元格的內容視圖的高度。 ( 嗯?點擊這裡

請記住,這個想法是讓單元格的子視圖垂直連接到單元格的內容視圖,以便他們“施加壓力”並使內容視圖展開以適應它們。 使用一個帶有幾個子視圖的示例單元格,以下是一些視圖(您並不是全部!)的約束條件需要看起來像:

你可以想像,隨著更多的文本被添加到上面的示例單元格中的多行身體標籤,它將需要垂直增長以適應文本,這將有效地強制單元格的高度增長。 (當然,你需要正確地得到約束條件才能正常工作!)

正確獲取約束條件絕對是獲得動態單元高度與Auto Layout配合使用的最難和最重要的部分 。 如果你在這裡犯了一個錯誤,它可能會阻止所有其他工作 - 所以請花時間! 我建議在代碼中設置約束條件,因為您確切知道哪些約束條件將被添加到哪裡,並且在出現問題時更易於調試。 在代碼中添加約束可以與使用佈局錨點的Interface Builder或GitHub上提供的奇妙的開源API之一一樣簡單並且功能強大得多。

  • 如果您在代碼中添加約束,您應該從UITableViewCell子類的updateConstraints方法內執行一次。 請注意, updateConstraints可能會被多次調用,因此為了避免多次添加相同的約束,請確保在updateConstraints中包含約束添加代碼,以檢查boolean屬性,如didSetupConstraints (在您之後將其設置為YES運行一次約束添加代碼)。 另一方面,如果你有更新現有約束的代碼(比如調整某些約束的constant屬性),可以將它放在updateConstraints但是在didSetupConstraints檢查之外,這樣它就可以在每次調用方法時運行。

2.確定唯一表格視圖單元重用標識符

對於單元中的每個唯一約束集,請使用唯一的單元重用標識符。 換句話說,如果您的單元格具有多個唯一佈局,則每個唯一佈局應該會收到自己的重用標識符。 (提示您需要使用新的重用標識符,這意味著您的單元格變體具有不同數量的子視圖,或者子視圖以不同的方式排列。)

例如,如果您在每個單元格中顯示電子郵件,則可能有4種獨特的佈局:僅包含主題的郵件,包含主題和正文的郵件,包含主題和照片附件的郵件以及包含主題的郵件,身體和照片附件。 每個佈局都有完全不同的約束條件來實現它,所以一旦單元格初始化並為這些單元格類型之一添加約束條件,單元格應該獲得特定於該單元格類型的唯一重用標識符。 這意味著,當您將某個單元出列以供重用時,約束條件已被添加並準備好用於該單元類型。

請注意,由於內在內容大小的差異,具有相同約束(類型)的單元格可能仍然具有不同的高度! 由於內容大小不同,不要將基本不同的佈局(不同的約束)與不同的計算視圖框架(根據相同的約束條件解決)混淆。

  • 不要將具有完全不同的約束集合的單元添加到同一個重用池(即使用相同的重用標識符),然後嘗試從每個出列後的臨時設置中刪除舊約束並設置新約束。 內部自動佈局引擎不適用於處理約束條件的大規模變更,您將看到大量的性能問題。

對於iOS 8 - 自我大小的單元格

3.啟用行高估計

要啟用自定義表格視圖單元格,您必須將表格視圖的rowHeight屬性設置為UITableViewAutomaticDimension。 您還必須將值分配給estimatedRowHeight屬性。 只要設置了這兩個屬性,系統就會使用自動佈局來計算行的實際高度

Apple: 使用自定義表格視圖單元格

在iOS 8中,Apple已經內化了以前在iOS 8之前必須執行的大部分工作。為了允許自定義大小的單元機制發揮作用,您必須首先將table view的rowHeight屬性設置為常量UITableViewAutomaticDimension 。 然後,您只需通過將表視圖的estimatedRowHeight屬性設置為非零值來啟用行高度估計,例如:

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

這是為表格視圖提供臨時估計/佔位符,用於尚未顯示在屏幕上的單元格的行高。 然後,當這些單元格即將在屏幕上滾動時,將計算實際行高度。 為了確定每行的實際高度,表格視圖會自動詢問每個單元格的內容視圖需要基於內容視圖的已知固定寬度(基於表格視圖的寬度,減去任何其他內容索引或附件視圖)以及您添加到單元格的內容視圖和子視圖的自動佈局約束。 一旦確定了此實際單元格高度,則會使用新的實際高度更新該行的舊估計高度(並根據需要對錶視圖的contentSize / contentOffset進行任何調整)。

一般而言,您提供的估算值不一定非常準確 - 它只用於在表格視圖中正確調整滾動指標的大小,並且表格視圖可以很好地調整滾動指標,從而得出錯誤的估計值在屏幕上滾動單元格。 您應該將表視圖(以viewDidLoad或類似方式)中的estimatedRowHeight屬性設置為“平均”行高的常量值。 只有當你的行高有極大的可變性(例如相差一個數量級),並且你在滾動時注意到滾動指標“跳躍”,你應該打擾實施tableView:estimatedHeightForRowAtIndexPath:做最小的計算,以返回更準確的估計每一行。

對於iOS 7支持(自己實現自動調整大小)

3.做一個佈局通過並獲取單元格高度

首先,實例化一個表視圖單元格的屏幕外實例, 每個重用標識符一個實例,嚴格用於高度計算。 (離屏是指單元格引用存儲在視圖控制器的屬性/ ivar中,並且永遠不會從tableView:cellForRowAtIndexPath:返回tableView:cellForRowAtIndexPath:用於表格視圖實際呈現在屏幕上。)接下來,必須為單元格配置確切的內容(例如文本,圖像等),如果它被顯示在表格視圖中,它將保持不變。

然後,強制單元格立即佈局其子視圖,然後使用UITableViewCellcontentView上的systemLayoutSizeFittingSize:方法來找出單元格所需的高度。 使用UILayoutFittingCompressedSize獲取適合單元格的所有內容所需的最小大小。 然後可以從tableView:heightForRowAtIndexPath: delegate方法返回高度。

4.使用估計的行高

如果你的表視圖中有幾十行,你會發現在第一次加載表視圖時,執行自動佈局約束求解可能會快速陷入主線程,因為每行都調用tableView:heightForRowAtIndexPath:在第一次加載時(為了計算滾動指示器的大小)。

從iOS 7開始,您可以(絕對應該)在表格視圖中使用estimatedRowHeight屬性。 這是為表格視圖提供臨時估計/佔位符,用於尚未顯示在屏幕上的單元格的行高。 然後,當這些單元格即將在屏幕上滾動時,將計算實際行高(通過調用tableView:heightForRowAtIndexPath: ,並使用實際高度更新估計高度。

一般而言,您提供的估算值不一定非常準確 - 它只用於在表格視圖中正確調整滾動指標的大小,並且表格視圖可以很好地調整滾動指標,從而得出錯誤的估計值在屏幕上滾動單元格。 您應該將表視圖(以viewDidLoad或類似方式)中的estimatedRowHeight屬性設置為“平均”行高的常量值。 只有當你的行高有極大的可變性(例如相差一個數量級),並且你在滾動時注意到滾動指標“跳躍”,你應該打擾實施tableView:estimatedHeightForRowAtIndexPath:做最小的計算,以返回更準確的估計每一行。

5.(如果需要)添加行高度緩存

如果你已經完成了上述所有工作,並且仍然發現在tableView:heightForRowAtIndexPath:執行約束求解時性能的速度緩慢得令人無法接受,那麼你不幸需要為單元高度實現一些緩存。 (這是Apple工程師建議的方法。)總體思路是讓Auto Layout引擎首次解決約束問題,然後緩存該單元格的計算高度,並將該緩存值用於該單元格高度的所有未來請求。 訣竅當然是確保在發生任何可能導致單元格高度發生變化的事情時清除單元格的高速緩存高度 - 主要是當單元格的內容髮生更改或發生其他重要事件時(如用戶調整動態類型文字大小滑塊)。

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 multi-line 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,請查看由@KentBoogaart組合的示例項目


Another "solution": skip all this frustration and use a UIScrollView instead to get a result that looks and feels identical to UITableView.

That was the painful "solution" for me, after having put in literally 20+ very frustrating hours total trying to build something like what smileyborg suggested and failing over many months and three versions of App Store releases.

My take is that if you really need iOS 7 support (for us, it's essential) then the technology is just too brittle and you'll pull your hair out trying. And that UITableView is complete overkill generally unless you're using some of the advanced row editing features and/or really need to support 1000+ "rows" (in our app, it's realistically never more than 20 rows).

The added bonus is that the code gets insanely simple versus all the delegate crap and back and forth that comes with UITableView. It's just one single loop of code in viewOnLoad that looks elegant and is easy to manage.

Here's some tips on how to do it:

1) Using either Storyboard or a nib file, create a ViewController and associated root view.

2) Drag over a UIScrollView onto your root view.

3) Add constraints top, bottom, left and right constraints to the top-level view so the UIScrollView fills the entire root view.

4) Add a UIView inside the UIScrollView and call it "container". Add top, bottom, left and right constraints to the UIScrollView (its parent). KEY TRICK: Also add a "Equal widths" constraints to link the UIScrollView and UIView.

You will get an error "scroll view has ambiguous scrollable content height" and that your container UIView should have a height of 0 pixels. Neither error seems to matter when the app is running.

5) Create nib files and controllers for each of your "cells". Use UIView not UITableViewCell.

5) In your root ViewController, you essentially add all the "rows" to the container UIView and programmatically add constraints linking their left and right edges to the container view, their top edges to either the container view top (for the first item) or the previous cell. Then link the final cell to the container bottom.

For us, each "row" is in a nib file. So the code looks something like this:

class YourRootViewController {

    @IBOutlet var container: UIView! //container mentioned in step 4

    override func viewDidLoad() {

        super.viewDidLoad()

        var lastView: UIView?
        for data in yourDataSource {

            var cell = YourCellController(nibName: "YourCellNibName", bundle: nil)
            UITools.addViewToTop(container, child: cell.view, sibling: lastView)
            lastView = cell.view
            //Insert code here to populate your cell
        }

        if(lastView != nil) {
            container.addConstraint(NSLayoutConstraint(
                item: lastView!,
                attribute: NSLayoutAttribute.Bottom,
                relatedBy: NSLayoutRelation.Equal,
                toItem: container,
                attribute: NSLayoutAttribute.Bottom,
                multiplier: 1,
                constant: 0))
        }

        ///Add a refresh control, if you want - it seems to work fine in our app:
        var refreshControl = UIRefreshControl()
        container.addSubview(refreshControl!)
    }
}

And here's the code for UITools.addViewToTop:

class UITools {
    ///Add child to container, full width of the container and directly under sibling (or container if sibling nil):
    class func addViewToTop(container: UIView, child: UIView, sibling: UIView? = nil)
    {
        child.setTranslatesAutoresizingMaskIntoConstraints(false)
        container.addSubview(child)

        //Set left and right constraints so fills full horz width:

        container.addConstraint(NSLayoutConstraint(
            item: child,
            attribute: NSLayoutAttribute.Leading,
            relatedBy: NSLayoutRelation.Equal,
            toItem: container,
            attribute: NSLayoutAttribute.Left,
            multiplier: 1,
            constant: 0))

        container.addConstraint(NSLayoutConstraint(
            item: child,
            attribute: NSLayoutAttribute.Trailing,
            relatedBy: NSLayoutRelation.Equal,
            toItem: container,
            attribute: NSLayoutAttribute.Right,
            multiplier: 1,
            constant: 0))

        //Set vertical position from last item (or for first, from the superview):
        container.addConstraint(NSLayoutConstraint(
            item: child,
            attribute: NSLayoutAttribute.Top,
            relatedBy: NSLayoutRelation.Equal,
            toItem: sibling == nil ? container : sibling,
            attribute: sibling == nil ? NSLayoutAttribute.Top : NSLayoutAttribute.Bottom,
            multiplier: 1,
            constant: 0))
    }
}

The only "gotcha" I've found with this approach so far is that UITableView has a nice feature of "floating" section headers at the top of the view as you scroll. The above solution won't do that unless you add more programming but for our particular case this feature wasn't 100% essential and nobody noticed when it went away.

If you want dividers between your cells, just add a 1 pixel high UIView at the bottom of your custom "cell" that looks like a divider.

Be sure to turn on "bounces" and "bounce vertically" for the refresh control to work and so it seems more like a tableview.

TableView shows some empty rows and dividers under your content, if it doesn't fill the full screen where as this solution doesn't. But personally, I prefer if those empty rows weren't there anyway - with variable cell height it always looked "buggy" to me anyway to have the empty rows in there.

Here's hoping some other programmer reads my post BEFORE wasting 20+ hours trying to figure it out with Table View in their own app. :)


As long as your layout in your cell is good.

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];

    return [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
}

Update: You should use dynamic resizing introduced in iOS 8.


I had to use dynamic views (setup views and constraints by code) and when I wanted to set preferredMaxLayoutWidth label's width was 0. So I've got wrong cell height.

Then I added

[cell layoutSubviews];

before executing

[cell setNeedsUpdateConstraints];
[cell updateConstraintsIfNeeded];

After that label's width was as expected and dynamic height was calculating right.


I just did some dumb try and error with the 2 values of rowHeight and estimatedRowHeight and just thought it might provide some debugging insight:

If you set them both OR only set the estimatedRowHeight you will get the desired behavior:

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 1.00001 // MUST be greater than 1

It's suggested that you do your best to get the correct estimate, but the end result isn't different. It will just affect your performance.

If you only set the rowHeight ie only do:

tableView.rowHeight = UITableViewAutomaticDimension

your end result would not be as desired:

If you set the estimatedRowHeight to 1 or smaller then you will crash regardless of the rowHeight .

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 1 

I crashed with the following error message:

Terminating app due to uncaught exception
'NSInternalInconsistencyException', reason: 'table view row height
must not be negative - provided height for index path (<NSIndexPath:
0xc000000000000016> {length = 2, path = 0 - 0}) is -1.000000'
    ...some other lines...

libc++abi.dylib: terminating with uncaught exception of type
NSException

If you do you layout programmatically, here is what to consider for iOS 10 using anchors in Swift.

There are three rules/ steps

NUMBER 1: set this two properties of tableview on viewDidLoad, the first one is telling to the tableview that should expect dynamic sizes on their cells, the second one is just to let the app calculate the size of the scrollbar indicator, so it helps for performance.

    tableView.rowHeight = UITableViewAutomaticDimension
    tableView.estimatedRowHeight = 100

NUMBER 2: This is important you need to add the subviews to the contentView of the cell not to the view, and also use its layoutsmarginguide to anchor the subviews to the top and bottom, this is a working example of how to do it.

override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    setUpViews()
}

private func setUpViews() {

    contentView.addSubview(movieImageView)
    contentView.addSubview(descriptionLabel)
    let marginGuide = contentView.layoutMarginsGuide

    NSLayoutConstraint.activate([
        movieImageView.heightAnchor.constraint(equalToConstant: 80),
        movieImageView.widthAnchor.constraint(equalToConstant: 80),
        movieImageView.leftAnchor.constraint(equalTo: marginGuide.leftAnchor),
        movieImageView.topAnchor.constraint(equalTo: marginGuide.topAnchor, constant: 20),

        descriptionLabel.leftAnchor.constraint(equalTo: movieImageView.rightAnchor, constant: 15),
        descriptionLabel.rightAnchor.constraint(equalTo: marginGuide.rightAnchor),
        descriptionLabel.bottomAnchor.constraint(equalTo: marginGuide.bottomAnchor, constant: -15),
        descriptionLabel.topAnchor.constraint(equalTo: movieImageView.topAnchor)

        ])
}

Create a method that will add the subviews and perform the layout, call it in the init method.

NUMBER 3: DON'T CALL THE METHOD:

  override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    }

If you do it you will override your implementation.

Follow this 3 rules for dynamic cells in tableviews.

here is a working implementation https://github.com/jamesrochabrun/MinimalViewController



In my case, the padding was because of the sectionHeader and sectionFooter heights, where storyboard allowed me to change it to minimum 1. So in viewDidLoad method:

tableView.sectionHeaderHeight = 0
tableView.sectionFooterHeight = 0

Let's say you have a cell with a subview, and you want the cell's height to be high enough to encompass the subview + padding.

1) Set the subview's bottom constraint equal to the cell.contentView minus the padding you want. Do not set constraints on the cell or cell.contentView itself.

2) Set either the tableView's rowHeight property or tableView:heightForRowAtIndexPath: to UITableViewAutomaticDimension .

3) Set either the tableView's estimatedRowHeight property or tableView:estimatedHeightForRowAtIndexPath: to a best guess of the height.

而已。


The solution proposed by @smileyborg is almost perfect. If you have a custom cell and you want one or more UILabel with dynamic heights then the systemLayoutSizeFittingSize method combined with AutoLayout enabled returns a CGSizeZero unless you move all your cell constraints from the cell to its contentView (as suggested by @TomSwift here How to resize superview to fit all subviews with autolayout? ).

To do so you need to insert the following code in your custom UITableViewCell implementation (thanks to @Adrian).

- (void)awakeFromNib{
    [super awakeFromNib];
    for (NSLayoutConstraint *cellConstraint in self.constraints) {
        [self removeConstraint:cellConstraint];
        id firstItem = cellConstraint.firstItem == self ? self.contentView : cellConstraint.firstItem;
        id seccondItem = cellConstraint.secondItem == self ? self.contentView : cellConstraint.secondItem;
        NSLayoutConstraint *contentViewConstraint =
        [NSLayoutConstraint constraintWithItem:firstItem
                                 attribute:cellConstraint.firstAttribute
                                 relatedBy:cellConstraint.relation
                                    toItem:seccondItem
                                 attribute:cellConstraint.secondAttribute
                                multiplier:cellConstraint.multiplier
                                  constant:cellConstraint.constant];
        [self.contentView addConstraint:contentViewConstraint];
    }
}

Mixing @smileyborg answer with this should works.


To set automatic dimension for row height & estimated row height, ensure following steps to make, auto dimension effective for cell/row height layout.

  • Assign and implement tableview dataSource and delegate
  • Assign UITableViewAutomaticDimension to rowHeight & estimatedRowHeight
  • Implement delegate/dataSource methods (ie heightForRowAt and return a value UITableViewAutomaticDimension to it)

-

Objective C:

// in ViewController.h
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>

  @property IBOutlet UITableView * table;

@end

// in ViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
    self.table.dataSource = self;
    self.table.delegate = self;

    self.table.rowHeight = UITableViewAutomaticDimension;
    self.table.estimatedRowHeight = UITableViewAutomaticDimension;
}

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {

    return UITableViewAutomaticDimension;
}

迅速:

@IBOutlet weak var table: UITableView!

override func viewDidLoad() {
    super.viewDidLoad()

    // Don't forget to set dataSource and delegate for table
    table.dataSource = self
    table.delegate = self

    // Set automatic dimensions for row height
    table.rowHeight = UITableViewAutomaticDimension
    table.estimatedRowHeight = UITableViewAutomaticDimension
}



// UITableViewAutomaticDimension calculates height of label contents/text
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    return UITableViewAutomaticDimension
}

For label instance in UITableviewCell

  • Set number of lines = 0 (& line break mode = truncate tail)
  • Set all constraints (top, bottom, right left) with respect to its superview/ cell container.
  • Optional : Set minimum height for label, if you want minimum vertical area covered by label, even if there is no data.

Note : If you've more than one labels (UIElements) with dynamic length, which should be adjusted according to its content size: Adjust 'Content Hugging and Compression Resistance Priority` for labels which you want to expand/compress with higher priority.


yet another iOs7+iOs8 solution in Swift

var cell2height:CGFloat=44

override func viewDidLoad() {
    super.viewDidLoad()
    theTable.rowHeight = UITableViewAutomaticDimension
    theTable.estimatedRowHeight = 44.0;
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell =  tableView.dequeueReusableCellWithIdentifier("myTableViewCell", forIndexPath: indexPath) as! myTableViewCell
    cell2height=cell.contentView.height
    return cell
}

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    if #available(iOS 8.0, *) {
        return UITableViewAutomaticDimension
    } else {
        return cell2height
    }
}

對於IOS8來說非常簡單:

override func viewDidLoad() {  
    super.viewDidLoad()

    self.tableView.estimatedRowHeight = 80
    self.tableView.rowHeight = UITableViewAutomaticDimension
}

要么

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return UITableViewAutomaticDimension
}

但對於IOS7,關鍵是自動佈局後計算高度,

func calculateHeightForConfiguredSizingCell(cell: GSTableViewCell) -> CGFloat {
    cell.setNeedsLayout()
    cell.layoutIfNeeded()
    let height = cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingExpandedSize).height + 1.0
    return height
}

重要

  • 如果有多行標籤,請不要忘記將numberOfLines設置為0

  • 不要忘了label.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds)

完整的示例代碼在here


tableView.estimatedRowHeight = 343.0
tableView.rowHeight = UITableViewAutomaticDimension





row-height