UICollectionView inside UITableViewCell - dynamic height?

One of our application screens requires us to place a UICollectionView inside a UITableViewCell . This UICollectionView will have a dynamic number of elements, as a result of which the height must also be calculated dynamically. However, I ran into problems trying to calculate the height of the embedded UICollectionView .

Our comprehensive UIViewController was created in Storyboards and uses automatic layout. But I do not know how to dynamically increase the height of a UITableViewCell depending on the height of the UICollectionView .

Can someone give some tips or advice on how to do this?

+100
ios uitableview uicollectionview
Jun 09 '14 at 18:39
source share
15 answers

The correct answer is YES , you CAN do it.

I ran into this problem a few weeks ago. This is actually easier than you think. Put your cells in the NIB (or storyboards) and attach them so that the auto layout does all the work.

Given the following structure:

Tableview

Tableviewcell

Collectionview

CollectionViewCell

CollectionViewCell

CollectionViewCell

[... a variable number of cells or different cell sizes]

The solution is to tell the auto layout to first calculate the size of the ViewCell collection and then the viewSize collection and use it as the size of your cell. This is the UIView method that does the magic:

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

Here you must specify the size of the TableViewCell, which in your case is a CollectionView contentSize.

CollectionViewCell

In CollectionViewCell, you must specify a cell in the layout every time you change the model (for example: you set UILabel with text, then the cell must be layout again).

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

Tableviewcell

TableViewCell does the magic. It has access to your CollectionView, allows you to automatically compose collectionView cells using the evaluatedItemSize UICollectionViewFlowLayout .

Then the trick is to set the size of the tableView cell in the systemLayoutSizeFittingSize method .... (NOTE: iOS8 or later)

NOTE. I tried using the tableView delegate cell height method -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath .but is too late for the automatic layout system to compute CollectionView contentSize, and sometimes you may find cells with incorrect resizing.

 @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 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 size has to be exactly the content // Size of the collection View, just return the // collectionViewLayout collectionViewContentSize. return [self.collectionView.collectionViewLayout collectionViewContentSize]; } // Other stuff here... @end 

Tableviewcontroller

Remember to enable the automatic layout system for tableView cells on your TableViewController :

 - (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; } 

CREDIT: @rbarbera helped sort this out

+108
Oct 27 '15 at 9:26
source share

I think my solution is much simpler than suggested by @PabloRomeu.

Step 1. Create an exit from the UICollectionView in the UITableViewCell subclass where the UICollectionView is located. Let this name be collectionView

Step 2. Add IB to limit the height of the UICollectionView and create an output for the UITableViewCell subclass . Let this name be collectionViewHeight .

Step 3. In tableView:cellForRowAtIndexPath: add code:

 // deque a cell cell.frame = tableView.bounds; [cell layoutIfNeeded]; [cell.collectionView reloadData]; cell.collectionViewHeight.constant = cell.collectionView.collectionViewLayout.collectionViewContentSize.height; 
+79
Jul 17. '16 at 21:33
source share

Both table views and collection views are subclasses of UIScrollView and therefore do not like to be embedded in another scroll view, as they try to calculate content sizes, reuse cells, etc.

I recommend that you use view collection only for all your purposes.

You can divide it into sections and “view” the layouts of some sections as a tabular view, and others as a collection. In the end, nothing you can achieve with a collection view that you can use with a table view.

If you have a basic grid layout for “parts” as a collection, you can also use regular table cells to process them. However, if you don’t need iOS 5 support, you better use collection views.

+19
Jun 16 '14 at 8:30
source share

The answer of Pablo Romeu above ( https://stackoverflow.com/a/12735/ ... ) really helped me in my problem. I had to do a few things differently, however, so that this worked for my problem. First, I did not need to call layoutIfNeeded() so often. I only needed to call it on collectionView in the systemLayoutSizeFitting function.

Secondly, I had automatic layout restrictions for my collection view in a table view cell to give it some addition. So I had to subtract targetSize.width and targetSize.width fields from targetSize.width when setting the width of collectionView.frame . I also had to add top and bottom margins to the return value of the CGSize height.

To get these constraint constants, I had the opportunity to either create outputs for the constraints, either hard code their constants, or find them by identifier. I decided to go with the third option to make my table view cell class easy to repeat. In the end, that was all I needed for it to work:

 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 } } 

As a helper function to get the identifier constraint, I add the following extension:

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

NOTE. You will need to set an identifier for these restrictions in your storyboard or where they are created. If they do not have constant 0, then it does not matter. Just like in Pablo's answer, you will need to use UICollectionViewFlowLayout as a layout to represent your collection. Finally, be sure to associate collectionView IBOutlet with your storyboard.

With the custom table view cell above, I can now subclass it in any other table view cell that needs the collection view and force it to implement the UICollectionViewDelegateFlowLayout and UICollectionViewDataSource . Hope this is helpful to someone else!

+7
Apr 26 '18 at 16:52
source share

I would put a static method in a collection view class that will return a size based on the content it will have. Then use this method in heightForRowAtIndexPath to return the correct size.

Also note that you may get some weird behavior when you insert these types of viewControllers. I did this once and had some strange memory problems that I never developed.

+2
Jun 09 '14 at 18:50
source share

Perhaps my option will be useful; I have been solving this problem for the past two hours. I do not claim to be 100% correct or optimal, but my skill is very small, but I would like to hear expert comments. Thank. One important note: this works for a static table - it is indicated in my current work. So, all I use is viewWillLayoutSubviews from tableView . And a little more.

 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 } } 
  • I know that the View collection is on the first line of the second section;
  • Let the row height be 17 p. greater than its content height;
  • iconsCellHeight is a random number when the program starts (I know that in portrait form it should be exactly 392, but that doesn't matter). If the contents of the View + 17 collection are not equal to this number, change its value. The next time in this situation, the condition gives FALSE;
  • After updating tableView. In my case, this is a combination of two operations (for a pleasant update of expanding strings);
  • And of course, in heightForRowAtIndexPath add one line for the code.
+2
May 25 '16 at 10:50
source share

An alternative to Pablo Romeu's solution is to configure the UICollectionView itself, rather than doing work in a table cell.

The main problem is that by default the collection view does not have its own size and therefore cannot report the automatic location of the sizes used. You can fix this by creating your own subclass that returns a useful internal size.

Subclass UICollectionView and override the following methods

 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() } 

(You must also override the associated methods: reloadSections, reloadItemsAtIndexPaths similarly to reloadData ())

The call to layoutIfNeeded causes the collection view to recalculate the size of the content, which can then be used as the new native size.

In addition, you need to explicitly handle view size changes (for example, when the device rotates) in the table view controller

  override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator) dispatch_async(dispatch_get_main_queue()) { self.tableView.reloadData() } } 
+2
Jul 19 '16 at 1:10
source share

The easiest approach I've ever come up with, Credits to @igor, answer above,

In your tableviewcell class just paste this

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

and of course change the viewoutlet collection with your outlet in the cell class

+2
Jan 08 '18 at 1:01
source share

I recently ran into the same problem and I almost tried every solution in the answers, some of them worked, and others were not interested in my approach to @PabloRomeu : if you have other content in the cell (except for the collection view) you will need to calculate them the height and height of their restrictions and return the result to get the correct automatic location, and I do not like to calculate things manually in my code. So here is a solution that worked fine for me without any manual calculations in my code.

in cellForRow:atIndexPath table view, I do the following:

 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 } 

I think what is happening here is that I am forcing the cell of the table view to adjust its height after the height of the collection view has been calculated. (after providing the collectionView date to the data source)

+2
Jun 14 '18 at 3:15
source share

I have read all the answers. This seems to serve all occasions.

 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 } 
+2
Dec 14 '18 at 16:03
source share

I get the idea from @Igor's post and invest my time in my project with swift

Just past this in your

  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 } 

Addition: If you see that your UICollectionView is intermittent when loading cells.

 func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { //do dequeue stuff cell.layer.shouldRasterize = true cell.layer.rasterizationScale = UIScreen.main.scale return cell } 
+2
May 01 '19 at 9:27
source share

Pablo's solution didn’t work very well for me, I had strange visual effects (the collection was incorrectly adjusted).

What worked was to adjust the height limit of collectionView (like NSLayoutConstraint) to collectionView contentSize during layoutSubviews() . This method is called when autolayout is applied to the cell.

 // 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() } 
0
Apr 05 '18 at 20:53
source share

In UITableViewDelegate:

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

Replace itemCount and CollectionViewCellHeight with real values. If you have an array of itemCount arrays could be:

 self.items[indexPath.row].count 

Or whatever.

-3
Jun 17 '14 at 0:29
source share

1. Create a dummy cell.
2. Use the collectionViewContentSize method for the UICollectionViewLayout UICollectionView using the current data.

-3
Jun 18 '14 at 10:26
source share

You can calculate the height of a collection based on its properties, such as itemSize , sectionInset , minimumLineSpacing , minimumInteritemSpacing , if your CollectionViewCell has a rule border.

-four
Jun 30 '15 at 6:49
source share



All Articles