UITableViewCell中的UICollectionView —动态高度?


125

我们的应用程序屏幕之一要求我们将放置在UICollectionView内部UITableViewCell。这UICollectionView将具有动态数量的项目,从而导致高度也必须动态计算。但是,我在尝试计算Embedded的高度时遇到了问题UICollectionView

我们的总体架构UIViewController是在情节提要中创建的,并且确实使用了自动布局。但是,我不知道如何UITableViewCell基于的高度动态增加的高度UICollectionView

谁能给一些技巧或建议,以实现这一目标?


您能否进一步说明您要完成的布局?它的高度UITableViewCell与其原始表格视图不同吗?您需要同时在UICollectionView和上垂直滚动吗?UITableView
apouche,2014年

3
基于作为表视图单元格一部分的UICollectionView的高度,UITableViewCell的高度应该是动态的。我的UICollectionView的布局是4列和动态的行数。因此,对于100个项目,我的UICollectionView中将有4列和25行。特定单元格的高度(如UITableView的heightForRowAtIndexPath所示)必须能够根据该UICollectionView的大小进行调整。这更清楚吗?
Shadowman 2014年

@Shadowman,请为我提出解决方案
Ravi Ojha

Answers:


127

正确的答案是YES,你CAN做到这一点。

几周前我遇到了这个问题。实际上,它比您想象的要容易。将您的单元格放入NIB(或情节提要)中并将其固定,以使自动布局完成所有工作

给出以下结构:

表格检视

TableViewCell

集合视图

CollectionViewCell

CollectionViewCell

CollectionViewCell

[...可变的细胞数或不同的细胞大小]

解决方案是告诉自动布局首先计算collectionViewCell的大小,然后计算集合视图的contentSize,然后将其用作单元格的大小。这是“做魔术” 的UIView方法:

-(void)systemLayoutSizeFittingSize:(CGSize)targetSize 
     withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority 
           verticalFittingPriority:(UILayoutPriority)verticalFittingPriority

您必须在此处设置TableViewCell的大小,在您的情况下为CollectionView的contentSize。

CollectionViewCell

CollectionViewCell上,每次更改模型时,您必须告诉单元进行布局(例如:使用文本设置UILabel,然后必须再次对单元进行布局)。

- (void)bindWithModel:(id)model {
    // Do whatever you may need to bind with your data and 
    // tell the collection view cell's contentView to resize
    [self.contentView setNeedsLayout];
}
// Other stuff here...

TableViewCell

TableViewCell确实神奇。它有一个出口,以您的的CollectionView,能够使用的CollectionView细胞自动布局estimatedItemSize的的UICollectionViewFlowLayout

然后,诀窍是在systemLayoutSizeFittingSize ...方法中设置tableView单元格的大小。(注意:iOS8或更高版本)

注:我试图用的tableView的代表单元的高度方法-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath。但为时已晚的自动布局系统来计算的CollectionView contentSize,有时你可能会发现错误调整大小的细胞。

@implementation TableCell

- (void)awakeFromNib {
    [super awakeFromNib];
    UICollectionViewFlowLayout *flow = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;

    // Configure the collectionView
    flow.minimumInteritemSpacing = ...;

    // This enables the magic of auto layout. 
    // Setting estimatedItemSize different to CGSizeZero 
    // on flow Layout enables auto layout for collectionView cells.
    // https://developer.apple.com/videos/play/wwdc2014-226/
    flow.estimatedItemSize = CGSizeMake(1, 1);

    // Disable the scroll on your collection view
    // to avoid running into multiple scroll issues.
    [self.collectionView setScrollEnabled:NO];
}

- (void)bindWithModel:(id)model {
    // Do your stuff here to configure the tableViewCell
    // Tell the cell to redraw its contentView        
    [self.contentView layoutIfNeeded];
}

// THIS IS THE MOST IMPORTANT METHOD
//
// This method tells the auto layout
// You cannot calculate the collectionView content size in any other place, 
// because you run into race condition issues.
// NOTE: Works for iOS 8 or later
- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority verticalFittingPriority:(UILayoutPriority)verticalFittingPriority {

    // With autolayout enabled on collection view's cells we need to force a collection view relayout with the shown size (width)
    self.collectionView.frame = CGRectMake(0, 0, targetSize.width, MAXFLOAT);
    [self.collectionView layoutIfNeeded];

    // If the cell's size has to be exactly the content 
    // Size of the collection View, just return the
    // collectionViewLayout's collectionViewContentSize.

    return [self.collectionView.collectionViewLayout collectionViewContentSize];
}

// Other stuff here...

@end

TableViewController

记住要在TableViewController上为tableView单元格启用自动布局系统:

- (void)viewDidLoad {
    [super viewDidLoad];

    // Enable automatic row auto layout calculations        
    self.tableView.rowHeight = UITableViewAutomaticDimension;
    // Set the estimatedRowHeight to a non-0 value to enable auto layout.
    self.tableView.estimatedRowHeight = 10;

}

信用:@rbarbera帮助解决了这个问题


4
是的 这可行。这应该是公认的答案。
csotiriou 2015年

1
任何机构都可以为我提供示例代码吗?我想在垂直集合视图,这是的tableview细胞内显示多个图像..
拉维欧嘉

4
最终,这对我来说是可行的,但是我不得不做另一个技巧:cell.bounds = CGRectMake(0, 0, CGRectGetWidth(tableView.bounds), 10000);这将“模拟”具有长collectionView的长单元格,因此collectionView将计算其所有单元格的大小。否则,collectionView仅计算可见的像元大小,并错误地设置talbeViewCell高度。
ingaham '16

20
当将框架高度设置为最大systemLayoutSizeFittingSize时,重新加载单元格时我迅速遇到问题,应用程序似乎在停滞了self.collectionView.layoutIfNeeded()。我通过将框架高度设置为1而不是最大高度来修复它。希望有人在这个问题上迷路,对您有所帮助。
泰坦

6
这是为我systemLayoutSizeFitting解决的问题collectionView.layoutIfNeeded()collectionView.frame = CGRect.init(origin: .zero, size: CGSize.init(width: targetSize.width, height: 1))
-in

101

我认为我的解决方案比@PabloRomeu提出的解决方案简单得多。

步骤1.在UICollectionView到的UITableViewCell subclass位置创建插座UICollectionView。让我们来命名collectionView

步骤2.添加IB来UICollectionView限制高度,并为此创建出口UITableViewCell subclass。让我们来命名为collectionViewHeight

步骤3.在tableView:cellForRowAtIndexPath:添加代码中:

// deque a cell
cell.frame = tableView.bounds;
[cell layoutIfNeeded];
[cell.collectionView reloadData];
cell.collectionViewHeight.constant = cell.collectionView.collectionViewLayout.collectionViewContentSize.height;

1
您实际尝试过吗?似乎既不可能在集合视图上设置高度约束,又不能将其固定在表格视图单元格的边缘。您要么得到的约束太少(因此警告大小为零),要么得到了冲突的约束(其中一个被忽略)。请说明如何为此解决方案设置约束。
戴尔

2
我有它的工作。我加约束collectionView到的各方面UITableViewCell,并设置tableView.estimatedRowHeight = 44;tableView.rowHeight = UITableViewAutomaticDimension;
伊戈尔

3
这就像冠军...简单高效...谢谢。如果某人无法正常工作,则可能是原因,我们需要将集合视图的底部绑定到表格视图单元的底部。然后它将开始工作。祝您编码愉快。:)
Sebin Roy

2
当调整屏幕大小(例如通过旋转iPad)时,这种方法是否可行?这可能会更改集合视图的高度,从而更改单元格的高度,但是如果在屏幕上,表格视图不一定会重新加载该单元格,那么就永远不要调用cellForRowAtIndexPath:方法,也不会给您机会更改约束常数。不过,可能有可能强制reloadData。
卡尔·林德伯格

2
您是否尝试过其他collectionView单元的大小?那是我的问题。如果您具有不同collectionCollection的像元大小,则无法使用... :(
Pablo Romeu

22

表格视图和集合视图都是UIScrollView子类,因此不希望被嵌入到另一个滚动视图中,因为它们试图计算内容大小,重用单元格等。

我建议您出于所有目的仅使用集合视图。

您可以将其划分为多个部分,然后将某些部分的布局作为表视图“处理”,而将其他部分作为集合视图“处理”。毕竟,使用集合视图无法实现的任何事情都无法通过表格视图实现。

如果您的集合视图“部分”具有基本的网格布局,则还可以使用常规表单元格来处理它们。不过,如果您不需要iOS 5支持,则最好使用集合视图。


1
您已经在引号中加上了“ treat”,因为您不能为每个CollectionView部分定义不同的布局,对吗?我还没有找到为不同的集合视图部分定义不同布局的选项。我是在俯视某些事情,还是在谈论我自己的UICollectionViewLayout的自定义子类中不同部分对“ layoutAttributesForItemAtIndexPath”的不同响应?
亚历克斯·弗朗卡

是->您是否在谈论对layoutAttributesForItemAtIndexPath我自己的自定义子类的不同部分做出不同的响应UICollectionViewLayout
里维拉2015年

2
@Rivera,您的回答最合理,听起来很正确。我在很多地方都读过这个建议。您是否还可以共享一些链接以采样或说明如何完成该操作或至少一些指针。TIA
sumsumsign 2015年

3
您的答案假设您是从头开始的,并且还没有适合您使用的大型复杂表格视图。该问题专门询问有关将collectionview放在tableview中的问题。海事组织,您的回应不是“答案”
xaphod

2
这是一个不好的答案。在表视图中包含集合视图是一件很常见的事情,几乎每个应用程序都可以做到。
crashoverride777

10

帕勃罗·罗梅(Pablo Romeu)的上述回答(https://stackoverflow.com/a/33364092/2704206)对我的问题有极大帮助。但是,我必须做一些不同的事情才能使它解决我的问题。首先,我不必layoutIfNeeded()经常打电话。我只需要collectionViewsystemLayoutSizeFitting函数中调用它。

其次,我在表格视图单元格中的集合视图上具有自动布局约束,以对其进行填充。因此,targetSize.width在设置collectionView.frame的宽度时,我不得不从减去前导和尾随边距。我还必须将顶部和底部边距添加到返回值的CGSize高度。

为了获得这些约束常量,我可以选择为约束创建出口,对它们的常量进行硬编码,或者通过标识符查找它们。我决定选择第三个选项,以使自定义表格视图单元格类易于重用。最后,这就是我需要使其工作的一切:

class CollectionTableViewCell: UITableViewCell {

    // MARK: -
    // MARK: Properties
    @IBOutlet weak var collectionView: UICollectionView! {
        didSet {
            collectionViewLayout?.estimatedItemSize = CGSize(width: 1, height: 1)
            selectionStyle = .none
        }
    }

    var collectionViewLayout: UICollectionViewFlowLayout? {
        return collectionView.collectionViewLayout as? UICollectionViewFlowLayout
    }

    // MARK: -
    // MARK: UIView functions
    override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        collectionView.layoutIfNeeded()

        let topConstraintConstant = contentView.constraint(byIdentifier: "topAnchor")?.constant ?? 0
        let bottomConstraintConstant = contentView.constraint(byIdentifier: "bottomAnchor")?.constant ?? 0
        let trailingConstraintConstant = contentView.constraint(byIdentifier: "trailingAnchor")?.constant ?? 0
        let leadingConstraintConstant = contentView.constraint(byIdentifier: "leadingAnchor")?.constant ?? 0

        collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width - trailingConstraintConstant - leadingConstraintConstant, height: 1)

        let size = collectionView.collectionViewLayout.collectionViewContentSize
        let newSize = CGSize(width: size.width, height: size.height + topConstraintConstant + bottomConstraintConstant)
        return newSize
    }
}

作为通过标识符检索约束的帮助函数,我添加了以下扩展名:

extension UIView {
    func constraint(byIdentifier identifier: String) -> NSLayoutConstraint? {
        return constraints.first(where: { $0.identifier == identifier })
    }
}

注意:您将需要在情节提要中或在创建约束的任何位置设置标识符。除非它们的常数为0,否则没关系。同样,如Pablo的回复一样,您将需要将其UICollectionViewFlowLayout用作收藏视图的布局。最后,确保将collectionViewIBOutlet 链接到情节提要。

使用上面的自定义表格视图单元格,我现在可以在需要集合视图并使其实现UICollectionViewDelegateFlowLayoutUICollectionViewDataSource协议的任何其他表格视图单元格中将其子类化。希望这对其他人有帮助!


5

我仔细阅读了所有答案。这似乎适用于所有情况。

override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        collectionView.layoutIfNeeded()
        collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width , height: 1)
        return collectionView.collectionViewLayout.collectionViewContentSize
}

3

Pablo Romeu解决方案的替代方法是自定义UICollectionView本身,而不是在表视图单元中进行工作。

潜在的问题是,默认情况下,集合视图没有内部尺寸,因此无法通知要使用的尺寸的自动布局。您可以通过创建确实返回有用的固有大小的自定义子类来对此进行补救。

创建UICollectionView的子类并覆盖以下方法

override func intrinsicContentSize() -> CGSize {
    self.layoutIfNeeded()

    var size = super.contentSize
    if size.width == 0 || size.height == 0 {
        // return a default size
        size = CGSize(width: 600, height:44)
    }

    return size
 }

override func reloadData() {
    super.reloadData()
    self.layoutIfNeeded()
    self.invalidateIntrinsicContentSize()
}

(您还应该以与reloadData()类似的方式重写相关方法:reloadSections,reloadItemsAtIndexPaths)

调用layoutIfNeeded强制收集视图重新计算内容大小,然后可以将其用作新的固有大小。

另外,您需要在表视图控制器中显式处理视图大小的更改(例如,在设备旋转时)

    override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)
{
    super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
    dispatch_async(dispatch_get_main_queue()) {
        self.tableView.reloadData()
    }
}

兄弟,您能帮我解决问题stackoverflow.com/questions/44650058/…吗?
5月Phyu

3

我最近遇到了同样的问题,我几乎尝试了答案中的每个解决方案,其中一些可行,而其他人对@PabloRomeu方法的主要关注不是,如果您在单元格中有其他内容(而不是集合视图)您将必须计算它们的高度和约束的高度,然后返回结果以正确设置自动布局,我不喜欢在代码中手动计算事物。因此,这里的解决方案对我来说很好用,而无需在代码中进行任何手动计算。

cellForRow:atIndexPath表格视图的中,我执行以下操作:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    //do dequeue stuff
    //initialize the the collection view data source with the data
    cell.frame = CGRect.zero
    cell.layoutIfNeeded()
    return cell
}

我认为这里发生的是,在计算了集合视图的高度之后,我迫使tableview单元调整其高度。(在将collectionView日期提供给数据源之后)


2

我将在集合视图类上放置一个静态方法,该方法将根据其拥有的内容返回大小。然后在中使用该方法heightForRowAtIndexPath以返回正确的大小。

还要注意,当嵌入这些类型的viewController时,您会得到一些奇怪的行为。我做了一次,并且遇到了一些我从未解决过的奇怪的内存问题。


对我来说,我试图将一个表视图控制器嵌入到集合视图控制器中。每次重复使用一个单元时,它最终都会记住我应用于它们的一些自动布局约束。这很奇怪,最终是一个可怕的想法。我想我将只使用单个集合视图。
fatuhoku

2

也许我的变体会很有用;在过去的两个小时中,我一直在决定执行此任务。我不假装它是100%正确或最佳的,但是我的技能还很小,我想听听专家的意见。谢谢。重要说明:此方法适用于静态表格-由我当前的工作指定。因此,所有我用的是viewWillLayoutSubviews的tableView。还有更多。

private var iconsCellHeight: CGFloat = 500 

func updateTable(table: UITableView, withDuration duration: NSTimeInterval) {
    UIView.animateWithDuration(duration, animations: { () -> Void in
        table.beginUpdates()
        table.endUpdates()
    })
}

override func viewWillLayoutSubviews() {
    if let iconsCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 1)) as? CategoryCardIconsCell {
        let collectionViewContentHeight = iconsCell.iconsCollectionView.contentSize.height
        if collectionViewContentHeight + 17 != iconsCellHeight {
            iconsCellHeight = collectionViewContentHeight + 17
            updateTable(tableView, withDuration: 0.2)
        }
    }
}

override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    switch (indexPath.section, indexPath.row) {
        case ...
        case (1,0):
            return iconsCellHeight
        default:
            return tableView.rowHeight
    }
}
  1. 我知道collectionView位于第二部分的第一行;
  2. 令行的高度为17 p。大于其内容高度;
  3. iconsCellHeight是程序启动时的随机数(我知道,在纵向形式下,它必须恰好是392,但这并不重要)。如果collectionView + 17的内容不等于此数字,请更改其值。下次在这种情况下条件为FALSE;
  4. 毕竟更新tableView。就我而言,它是两个操作的组合(用于很好地更新扩展行);
  5. 当然,在heightForRowAtIndexPath中,将一行添加到代码中。

2

到目前为止,我想出的最简单的方法是,上述@igor答案表示感谢,

在您的tableviewcell类中,只需插入

override func layoutSubviews() {
    self.collectionViewOutlet.constant = self.postPoll.collectionViewLayout.collectionViewContentSize.height
}

当然,更改单元格类中的集合视图出口与您的出口


1
细胞的高度不正确计算
聂KOV

2

我从@Igor帖子中得到了一些想法,并迅速投入了时间来进行我的项目

刚刚过去了

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    //do dequeue stuff
    cell.frame = tableView.bounds
    cell.layoutIfNeeded()
    cell.collectionView.reloadData()
    cell.collectionView.heightAnchor.constraint(equalToConstant: cell.collectionView.collectionViewLayout.collectionViewContentSize.height)
    cell.layoutIfNeeded()
    return cell
}

另外: 如果在加载单元格时看到UICollectionView不稳定。

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {       
  //do dequeue stuff
  cell.layer.shouldRasterize = true
  cell.layer.rasterizationScale = UIScreen.main.scale
  return cell
}

0

Pablo的解决方案对我来说不是很好,我有奇怪的视觉效果(collectionView无法正确调整)。

有效的方法是在期间将collectionView的高度限制(作为NSLayoutConstraint)调整为collectionView contentSize layoutSubviews()。这是将自动布局应用于单元格时调用的方法。

// Constraint on the collectionView height in the storyboard. Priority set to 999.
@IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint!


// Method called by autolayout to layout the subviews (including the collectionView).
// This is triggered with 'layoutIfNeeded()', or by the viewController
// (happens between 'viewWillLayoutSubviews()' and 'viewDidLayoutSubviews()'.
override func layoutSubviews() {

    collectionViewHeightConstraint.constant = collectionView.contentSize.height
    super.layoutSubviews()
}

// Call `layoutIfNeeded()` when you update your UI from the model to trigger 'layoutSubviews()'
private func updateUI() {
    layoutIfNeeded()
}

-3

在您的UITableViewDelegate中:

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return ceil(itemCount/4.0f)*collectionViewCellHeight;
}

用实际值替换itemCount和CollectionViewCellHeight。如果您有一个数组,则itemCount可能是:

self.items[indexPath.row].count

管他呢。


我认为主要问题是如何collectionViewCellHeight 在渲染之前进行计算。
sumsumsign

-3

1.创建虚拟单元格。
2.使用当前数据在UICollectionView的UICollectionViewLayout上使用collectionViewContentSize方法。


这应该是最好的答案...因为有没有可能设置为“uicollectionview”单元格的高度,而无需把内容以虚拟单元,获得高度,然后再次将其设置到适当的细胞
巴特洛梅耶Semańczyk

我该如何使用该方法?
xtrinch '16

添加一个示例。
Nik Kov '18

-5

您可以根据其属性,如计算征收的高度itemSizesectionInsetminimumLineSpacingminimumInteritemSpacing,如果你collectionViewCell有一个规则的边界。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.