Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UICollectionView inside a UITableViewCell -- dynamic height?

People also ask

How to make CollectionView height dynamic?

Make-CollectionView-Height-Dynamic-According-to-ContentAdd a height constraint to your collection view. Set its priority to 999. Set its constant to any value that only makes it reasonably visible on the storyboard. Change the bottom equal constraint of the collection view to greater or equal.

What is the difference between UITableView and UICollectionView?

Tableiw is a simple list, which displays single-dimensional rows of data. It's smooth because of cell reuse and other magic. 2. UICollectionView is the model for displaying multidimensional data .

What is a UICollectionView?

An object that manages an ordered collection of data items and presents them using customizable layouts.


The right answer is YES, you CAN do this.

I came across this problem some weeks ago. It is actually easier than you may think. Put your cells into NIBs (or storyboards) and pin them to let auto layout do all the work

Given the following structure:

TableView

TableViewCell

CollectionView

CollectionViewCell

CollectionViewCell

CollectionViewCell

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

The solution is to tell auto layout to compute first the collectionViewCell sizes, then the collection view contentSize, 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

You have to set here the size of the TableViewCell, which in your case is the CollectionView's contentSize.

CollectionViewCell

At the CollectionViewCell you have to tell the cell to layout each time you change the model (e.g.: you set a UILabel with a text, then the cell has to be layout again).

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

The TableViewCell does the magic. It has an outlet to your collectionView, enables the auto layout for collectionView cells using estimatedItemSize of the UICollectionViewFlowLayout.

Then, the trick is to set your tableView cell's size at the systemLayoutSizeFittingSize... method. (NOTE: iOS8 or later)

NOTE: I tried to use the delegate cell's height method of the tableView -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath.but it's too late for the auto layout system to compute the CollectionView contentSize and sometimes you may find wrong resized cells.

@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

Remember to enable the auto layout system for the tableView cells at 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 to sort this out


I think my solution is much simpler than the one proposed by @PabloRomeu.

Step 1. Create outlet from UICollectionView to UITableViewCell subclass, where UICollectionView is placed. Let, it's name will be collectionView

Step 2. Add in IB for UICollectionView height constraint and create outlet to UITableViewCell subclass too. Let, it's name will 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;

Both table views and collection views are UIScrollView subclasses and thus don't like to be embedded inside another scroll view as they try to calculate content sizes, reuse cells, etc.

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

You can divide it in sections and "treat" some sections' layout as a table view and others as a collection view. After all there's nothing you can't achieve with a collection view that you can with a table view.

If you have a basic grid layout for your collection view "parts" you can also use regular table cells to handle them. Still if you don't need iOS 5 support you should better use collection views.


Pablo Romeu's answer above (https://stackoverflow.com/a/33364092/2704206) helped me immensely with my issue. I had to do a few things differently, however, to get this working for my problem. First off, I didn't have to call layoutIfNeeded() as often. I only had to call it on the collectionView in the systemLayoutSizeFitting function.

Secondly, I had auto layout constraints on my collection view in the table view cell to give it some padding. So I had to subtract the leading and trailing margins from the targetSize.width when setting the collectionView.frame's width. I also had to add the top and bottom margins to the return value CGSize height.

To get these constraint constants, I had the option of either creating outlets to the constraints, hard-coding their constants, or looking them up by an identifier. I decided to go with the third option to make my custom table view cell class easily reusable. In the end, this was everything I needed to get it working:

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 retrieve a constraint by identifier, 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 the identifier on these constraints in your storyboard, or wherever they are being created. Unless they have a 0 constant, then it doesn't matter. Also, as in Pablo's response, you will need to use UICollectionViewFlowLayout as the layout for your collection view. Finally, make sure you link the collectionView IBOutlet to your storyboard.

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


I read through all the answers. This seems to serve all cases.

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
}