56
votes

How do I properly resize a UICollectionView so that it fully displays its contents? I have tried many things, including setting its frame, calling reloadData and invalidating the layout:

self.collectionView.contentSize = CGSizeMake(300, 2000);
self.collectionView.frame = CGRectMake(0, 0, 300, 2000);
[self.collectionView reloadData];
[self.collectionView.collectionViewLayout invalidateLayout];

but none of this has any effect. After pressing the button I still see the initial view, like this:

collection view only showing part of the content after frame resize

I have a small demo program where I have a data source producing 100 elements. In Interface Builder I initially set the size of the UICollectionView to a small value so that not all elements fit, after that I press a button after which the code above is executed. I expect the UICollectionView to now show all elements, but it doesn't.

EDIT: The demo program can be found at https://github.com/mjdemilliano/TestUICollectionView.

EDIT2: I have observed that the frame update is lost at some point, because if I press the button again, the current frame is back to the old value. After adding some log statements in the button event handler, the log output is:

before: frame = {{0, 58}, {320, 331}}, contentSize = {320, 1190}
update button pressed
after: frame = {{0, 0}, {300, 2000}}, contentSize = {300, 2000}
before: frame = {{0, 58}, {320, 331}}, contentSize = {320, 1190}
update button pressed
after: frame = {{0, 0}, {300, 2000}}, contentSize = {300, 2000}

I don't understand why the frame change is not kept, what is changing it.

At some point I will replace the hardcoded values by values obtained from the flow layout, but I wanted to rule that out and keep my example as simple as possible.

Context: What I want to do eventually is the following: I have a scrollable view with various controls like labels and images, and a collection view with dynamic content. I want to scroll all that, not just the collection view, therefore I am not using the collection view's own scrolling facilities, which work fine.

9
When you say your code has no effect — you mean that literally? Setting the contentSize is the correct way to set the scrollable area. If you can't scroll then you're failing to set contentSize. Are you sure self.collectionView is non-nil at the time that you make the call? Maybe you're accidentally calling before viewDidLoad?Tommy
@Tommy: I added a link to the full test project. The code is run after viewDidLoad because it is run when pushing the button. Nothing happens when the button is pushed, but log statements confirm that the values were updated. Yet I see no effect of changing either contentSize or frame of the collection view. Note that I have disabled scrolling of the collection view itself, because I want the collection view itself to be shown in full, which is then to be wrapped in a scrollview later on once this works.Martijn de Milliano
Martijn, could you give me an updated link to your test project? The github link specified doesn't work, and I want to do something very much like what you describe here.John Michael Zorko
@JohnMichaelZorko Oops... I had just removed it from Github two weeks ago in a spontaneous cleaning frenzy, unaware that I had referred to it from this question. Sorry! I have recreated it from what I think was my local clone of this test project, hopefully it helps you!Martijn de Milliano
Posted an answer to a similar question: stackoverflow.com/a/67055114/855680MLQ

9 Answers

111
votes

I solved this eventually by fixing all Auto Layout issues, fixing the height of the collection view using a constraint. Then, whenever I know the content has changed I update the value of the constraint using the value collectionView.contentSize.height:

self.verticalLayoutConstraint.constant = self.collectionView.contentSize.height;

Then the collection view is resized properly and it behaves nicely within the overall scrollview. I have updated the GitHub test project with my changes.

To me, doing this by updating the constraint manually instead of being able to tell iOS: "make the frame height of the collection view as large as needed" does not feel right to me, but it's the best I have come up with so far. Please post a better answer if you have one.

13
votes

It seems to work nicely with a custom UICollectionView class.

class AutoSizedCollectionView: UICollectionView {

    override var contentSize: CGSize {
        didSet {
            invalidateIntrinsicContentSize()
        }
    }

    override var intrinsicContentSize: CGSize {
        layoutIfNeeded()
        return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
    }
}

Set your custom class in the interface builder:

enter image description here

This way you can also set your collection views intrinsic size to 'placeholder' in interface builder to avoid having to set a height constraint.

enter image description here

I hope this helps someone else.

3
votes

Here's my implementation in Swift 3:

override func sizeThatFits(_ size: CGSize) -> CGSize {
    if (self.superview != nil) {
        self.superview?.layoutIfNeeded()
    }

    return collectionView.contentSize
}
2
votes
UICollectionViewFlowLayout *flowLayout;
flowLayout = [[UICollectionViewFlowLayout alloc]init];
[flowLayout setScrollDirection:UICollectionViewScrollDirectionVertical];
[flowLayout setMinimumInteritemSpacing:0.0f];
[flowLayout setMinimumLineSpacing:0.0f];
[self.collectionView setPagingEnabled:NO];
[flowLayout setItemSize:CGSizeMake(322.0, 148.0)];  //important to leave no white space between the images
[self.collectionView setCollectionViewLayout:flowLayout];

I found that autolayout in the storyboard is not helping too much. A correct setting for the UICollectionViewFlowLayout for your collectionView is the real help. If you adjust item size with setItemSize, you may get the result you want.

1
votes

Here's a way to bind the CollectionView's height via it's intrinsic size. I used it to properly size a CollectionView inside a TableView Cell (with dynamic cells height). and it works perfectly.

First, add this to your UICollectionView subclass:

override var intrinsicContentSize: CGSize {
    get {
        return self.contentSize
    }
}

Then call layoutIfNeeded() after you reload data:

reloadData()
layoutIfNeeded()
1
votes

The simplest method I found is to override sizeThatFits: methods as is:

- (CGSize)sizeThatFits:(CGSize)size
{
    if( self.superview )
        [self.superview layoutIfNeeded]; // to force evaluate the real layout

    return self.collectionViewLayout.collectionViewContentSize;
}
1
votes
  1. Add IBOutlet for CollectionView Height Constraint --> Like @IBOutlet weak var collectionViewHeight: NSLayoutConstraint!
  2. Add Below snipped code.

enter image description here

0
votes

You can try out my custom AGCollectionView class

  • Assign a height constraint of collectionView using a storyboard or programmatically.

- Assign this class to your UICollectionView.

class AGCollectionView: UICollectionView {

    fileprivate var heightConstraint: NSLayoutConstraint!

    override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
        self.associateConstraints()
    }

    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.associateConstraints()
    }

    override open func layoutSubviews() {
        super.layoutSubviews()

        if self.heightConstraint != nil {
            self.heightConstraint.constant = floor(self.contentSize.height)
        }
        else{
            self.sizeToFit()
            print("Set a heightConstraint set size to fit content")
        }
    }

    func associateConstraints() {
        // iterate through height constraints and identify

        for constraint: NSLayoutConstraint in constraints {
            if constraint.firstAttribute == .height {
                if constraint.relation == .equal {
                    heightConstraint = constraint
                }
            }
        }
    }
}
-3
votes

For me it is even simpler I think

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath

{
//add following code line after adding cells, before Return
...........
.........
scrollView.contentSize = = collectionView.contentSize;

//now scrollView size is equal to collectionView size. No matter how small or big it is.

return cell;
}