91

I would like the UICollectionView (The red one) to shrink to the height of the content size in this case UICollectionViewCells(the yellow ones) because there is a lot of empty space. What I tried is to use:

override func layoutSubviews() {
    super.layoutSubviews()
    if !__CGSizeEqualToSize(bounds.size, self.intrinsicContentSize) {
        self.invalidateIntrinsicContentSize()
    }
}

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

but return self.collection.contentSize always return (width, 0) and for this reason it shrinks too much to value of height 30 (The value which I set in the XIB file for the height, although I have constaint >= 30).

enter image description here

hasan
  • 22,029
  • 10
  • 58
  • 94
charbinary
  • 1,495
  • 3
  • 13
  • 23

16 Answers16

190

I would suggest the following:

  1. Add a height constraint to your collection view.
  2. Set its priority to 999.
  3. Set its constant to any value that makes it reasonably visible on the storyboard.
  4. Change the bottom equal constraint of the collection view to greater or equal.
  5. Connect the height constraint to an outlet.
  6. Every time you reload the data on the collection view do the following:

You may also want to consider the Inset of the collection view by adding it to the content size.

Code Sample:

CGFloat height = myCollectionView.collectionViewLayout.collectionViewContentSize.height
heightConstraint.constant = height
self.view.setNeedsLayout() Or self.view.layoutIfNeeded()

Explanation: Extra, You don't have to read if you understand it. obviously!!

The UI will try to reflect all the constraints no matter what are their priorities. Since there is a height constraint with lower priority of (999), and a bottom constraint of type greater or equal. whenever, the height constraint constant is set to a value less than the parent view height the collection view will be equal to the given height, achieving both constraints.

But, when the height constraint constant set to a value more than the parent view height both constraints can't be achieved. Therefore, only the constraint with the higher priority will be achieved which is the greater or equal bottom constraint.

The following is just a guess from an experience. So, it achieves one constrant. But, it also tries to make the error in the resulted UI for the other un-achieved lower priority constraint as lowest as possible. Therefore, the collection view height will be equal to the parent view size.

hasan
  • 22,029
  • 10
  • 58
  • 94
  • Mine expands to some extent but not perfect. It misses about 3 4 lines of the label. Any idea? Here is how it looks: https://ibb.co/jeO9Rv https://ibb.co/iV8b6v – nr5 Sep 07 '17 at 08:58
  • @Nil Do you have a header or footer on your collection view? the height of the footer and the header must be added. – hasan Sep 07 '17 at 11:12
  • Nevermind, it worked. The height constraint was set to proportional height that's why setting just only the constant was making some difference not much. – nr5 Sep 07 '17 at 11:24
  • While we are talking, do you have any idea how to pin a cell to the top? In my horizontal collectionview some cells are not pinned to the top. Just randomly floating in middle. Here: https://ibb.co/fHbSwv – nr5 Sep 07 '17 at 11:27
  • @hasan83 I am getting some extra height of collection view by this way, any suggestion ? – Mitesh Dobareeya Dec 27 '17 at 06:03
  • @MiteshDobareeya hmm, maybe its something with the collection view headers and footers? did you add any view in the collection view on the storyboard? – hasan Dec 27 '17 at 06:18
  • or maybe its with point 4. Change the bottom equal constraint of the collection view to greater or equal. Greater or equal which means the height of the collectionview should always be less or equal to the height of its parent view – hasan Dec 27 '17 at 06:18
  • @hasan83 No i have not added any view in collection view or no any header and footers are there. In my design i have just scroll view and collection view is inside content view of scrollview and 2 other views. All have fix height and collection view has fix height outlet. So when i reloading collection view, i am just getting hight based on it's content and assigning to its height constant. But unfortunately getting some extra height. – Mitesh Dobareeya Dec 27 '17 at 06:44
  • what I proposed in my answer doesn't work as it is on a scrollview. thats the problem. you got the idea on how you can do that on a view. so, think the same to figure out a solution for your situation – hasan Dec 27 '17 at 06:58
  • 1
    Like others in the comments I was getting some extra height when using this method. To fix this I simply did a `.reloadData()` and then changed the constant of the height constraint within a collection view `performBatchUpdates`. – Carl Feb 25 '19 at 11:58
  • Are you able to use this programmatically as well, or just with storyboards? – ArielSD Aug 16 '19 at 16:40
  • You can add the constraints programmatically. if that what you mean @ArielSD we already have some Code involved even with the storyboard version! – hasan Aug 16 '19 at 17:22
  • @hasan The collectionView I'm using is nested in a stackView in a tableViewCell - where should I call `setNeedsLayout`? – ArielSD Aug 16 '19 at 18:32
  • If you find the contentsize returned is higher than actual, set the initial height constant to something large like 1000. – RunLoop Jan 21 '21 at 12:43
  • I use RxSwift to detect when the height of the collection view changes (like when I configure it by calling reloadData after updating the relevant data). In the closure that is invoked by the observer, I do the update of the height constraint and call layoutIfNeeded - also on the superviews. The code for the observer is: collectionView.rx.observe(CGSize.self , "contentSize").subscribe(onNext: { (size) in } – Andy Weinstein May 20 '21 at 19:39
18

1) Set Fix Height of your CollectionView.

2) Create Outlet of this CollectionView Height Constant. Like :

IBOutlet NSLayoutConstraint *constHeight;

3) Add below method in your .m file:

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    CGFloat height = collectionMenu.collectionViewLayout.collectionViewContentSize.height;
    constHeight.constant = height;
}
Parth Patel
  • 808
  • 8
  • 26
15

I ended up, by subclassing the UICollectionView and overriding some methods as follows.

  • Returning self.collectionViewLayout.collectionViewContentSize for intrinsicContentSize makes sure, to always have the correct size
  • Then just call it whenever it might change (like on reloadData)

Code:

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

override var intrinsicContentSize: CGSize {
    return self.collectionViewLayout.collectionViewContentSize
}

But be aware, that you lose "cell re-using", if you display large sets of data, eventhough they don't fit on the screen.

chris
  • 15,435
  • 8
  • 34
  • 38
d4Rk
  • 6,007
  • 5
  • 40
  • 54
  • This works to make the `UICollectionView` initially the size of its content, but if the content changes ie: insert or delete rows, it doesn't resize. And if I call reload data the collection view stutters as it resizes. Any way to stop this stuttering? – cabyambo Jul 09 '19 at 15:17
  • Did you try calling `self.invalidateIntrinsicContentSize()` without calling `reloadData()`? Maybe this could work. – d4Rk Jul 23 '19 at 08:14
  • 1
    By far the most beautiful solution - at least for my use case. Thank you. It solves the problem of timing, layout needs all other constraints and views to be sized to determine content size - this one just works. – n13 Aug 04 '19 at 18:59
  • 1
    Actually - I had to constrain the intrinsic size to be at least 1 - never 0 - for this to correctly work. Otherwise there were small errors in the calculation, cutting off the bottom of the view by a few pixels. – n13 Aug 04 '19 at 19:22
  • Why does this mean you lose cell re-use? Not clear on what is causing that using this method. – AnthonyMDev Dec 12 '19 at 19:47
  • 1
    @AnthonyMDev Because using this approach, you're resizing the collection view to fit all items at the same time, so all items are loading any time. – d4Rk Dec 13 '19 at 08:30
  • Ahhhh that makes sense. I was thinking it was breaking the actual dequeue logic, but it's just a fact of the collection view displaying all the cells at all times. Thanks for the response! – AnthonyMDev Dec 18 '19 at 19:48
  • @cabyambo Were you able to find the solution for it? – Pranav Pravakar Jan 08 '21 at 14:57
15

In Swift 5 and Xcode 10.2.1

  1. Fix Height of the CollectionView

  2. Create Outlet of your CollectionViewHeight

    IBOutlet weak var myCollectionViewHeight: NSLayoutConstraint!
    
  3. Use below code

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        let height = myCollectionView.collectionViewLayout.collectionViewContentSize.height
        myCollectionViewHeight.constant = height
        self.view.layoutIfNeeded()
    }
    
iOS
  • 11,881
  • 4
  • 71
  • 92
10

You have to set height constraint as equal to content size

HeightConstraint.constant = collection.contentSize.height 
Bhanupriya
  • 1,164
  • 1
  • 8
  • 18
7

This seemed like the simplest solution for me.

class SelfSizingCollectionView: UICollectionView {
    override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
        commonInit()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }

    private func commonInit() {
        isScrollEnabled = false
    }

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

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

    override var intrinsicContentSize: CGSize {
        return contentSize
    }
}

You may not need to override reloadData

Tyten
  • 203
  • 2
  • 5
3

Do following.

  1. first set height constrain for UICollectionView
  2. here calendarBaseViewHeight is UICollectionView height Variable
  3. call the function after reload the collection view

    func resizeCollectionViewSize(){  
           calendarBaseViewHeight.constant = collectionView.contentSize.height      
     }
    
Prashant Tukadiya
  • 13,804
  • 3
  • 55
  • 78
parthiban
  • 95
  • 9
3

Took the solution by d4Rk which is great, except in my case it would keep cutting off the bottom of my collection view (too short). I figured out this was because intrinsic content size was sometimes 0 and this would throw off the calculations. IDK. All I know is this fixed it.

import UIKit

class SelfSizedCollectionView: UICollectionView {
    override func reloadData() {
        super.reloadData()
        self.invalidateIntrinsicContentSize()
    }

    override var intrinsicContentSize: CGSize {
        let s = self.collectionViewLayout.collectionViewContentSize
        return CGSize(width: max(s.width, 1), height: max(s.height,1))
    }

}
n13
  • 6,568
  • 48
  • 40
2

first of all calculate number of cells than multiply it with height of cell and then return height in this method

    collectionView.frame = CGRectMake (x,y,w,collectionView.collectionViewLayout.collectionViewContentSize.height); //objective c
    //[collectionView reloadData];
   collectionView.frame = CGRect(x: 0, y: 0, width: width, height: collectionView.collectionViewLayout.collectionViewContentSize.height) // swift
Matloob Hasnain
  • 919
  • 5
  • 20
2
  1. Subclass UICollectionView as follows
  2. Delete height constraint if any
  3. Turn on Intrinsic Size

-

class ContentSizedCollectionView: UICollectionView {
    override var contentSize:CGSize {
        didSet {
            invalidateIntrinsicContentSize()
        }
    }

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

On your UICollectionView set your constraints such as Trailing, Leading, and Bottom:

UICollectionView constraints

If you look at my height constraint in more detail, as it is purely for storyboard look so I don't get errors, I have it to Remove at build time. The real height constraint is set in my code down below.

UICollectionView height constraint

My code for DrawerCollectionView, which is set as the collection view Custom Class:

UICollectionView custom class

import UIKit

class DrawerCollectionView: UICollectionView {

    override func didMoveToSuperview() {
        super.didMoveToSuperview()

        heightAnchor.constraint(equalToConstant: contentSize.height).isActive = true
    }
}
George_E
  • 8,913
  • 3
  • 35
  • 76
  • Tried, but that didn't work for me. The other subclassing solution by d4Rk worked with minor modifications – n13 Aug 04 '19 at 19:26
0

work for me

    let heightRes = resCollectionView.collectionViewLayout.collectionViewContentSize.height
    foodHeightConstrant.constant = height.advanced(by: 1 )

    foodCollectionView.setNeedsLayout()
    foodCollectionView.layoutIfNeeded()
0

If you set the height constraint of the collection view. Just observe the contentSize change in the viewDidLoad and update the constraint.

        self.contentSizeObservation = collectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] collectionView, change in
            guard let `self` = self else { return }
            guard self.collectionView.contentSize != .zero else { return }
            self.collectionViewHeightLayoutConstraint.constant = self.collectionView.contentSize.height
        }
user25917
  • 589
  • 4
  • 12
0

I have a multi-line, multi-selection UICollectionView subclass where the cells are of fixed height and left-aligned flowing from left to right. It's embedded in a vertical stack view that's inside a vertical scroll view. See the UI component below the label "Property Types".

enter image description here

In order for the collection view to fit the height of its contentSize, here's what I had to do (note that this is all within the UICollectionView subclass):

  1. Give the collection view a non-zero minimum height constraint of priority 999. Auto-sizing the collection view to its content height simply won't work with zero height.

    let minimumHeight = heightAnchor.constraint(greaterThanOrEqualToConstant: 1)
    minimumHeight.priority = UILayoutPriority(999)
    minimumHeight.isActive = true
    
  2. Set the collection view's content hugging priority to .required for the vertical axis.

    setContentHuggingPriority(.required, for: .vertical)
    
  3. Calling reloadData() is followed by the following calls:

    invalidateIntrinsicContentSize()
    setNeedsLayout()
    layoutIfNeeded()
    

    For example, I have a setItems() function in my subclass:

    func setItems(_ items: [Item]) {
      self.items = items
      selectedIndices = []
      reloadData()
      invalidateIntrinsicContentSize()
      setNeedsLayout()
      layoutIfNeeded()
    }
    
  4. Override contentSize and intrinsicContentSize as follows:

    override var intrinsicContentSize: CGSize {
      return contentSize
    }
    
    override var contentSize: CGSize {
      didSet {
        invalidateIntrinsicContentSize()
        setNeedsLayout()
        layoutIfNeeded()
      }
    }
    
MLQ
  • 12,329
  • 12
  • 79
  • 130
0

Adjusting height of UICollectionView to the height of it's content size

SWIFT 5

final class MyViewController: UIViewController {

    // it's important to declare layout as separate constant due to late update in viewDidLayoutSubviews()
    private let layout = UICollectionViewFlowLayout()
    private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)


    override func viewDidLoad() {
        super.viewDidLoad()
    
        setupCollectionView()
        setupCollectionViewConstraints()
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
    
        updateFlowLayout()
    }

    private func setupCollectionView() {
        view.addSubview(collectionView)
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "UICollectionViewCell")
        collectionView.dataSource = self
    }

    private func setupCollectionViewConstraints() {
        // your collectionView constraints setup
    }

    private func updateFlowLayout() {
        let height = collectionView.collectionViewLayout.collectionViewContentSize.height
        layout.itemSize = CGSize(width: view.frame.width, height: height)
        layout.scrollDirection = .horizontal
        layout.minimumInteritemSpacing = .zero
        layout.minimumLineSpacing = .zero
        layout.sectionInset = UIEdgeInsets.zero
    }
}

extension MyViewController: UICollectionViewDataSource {
     func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {...}
     func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {...}
}
janaz
  • 534
  • 4
  • 11
-1

Get the height of the cell. Something like this

let cellHeight = cell.frame.height

Get the origin of the collection view

let cvOrigin = collectionView.frame.origin

Get the width of the collection view

let cvWidth = collectionView.bounds.width

Set the frame of the content view

collection.frame = CGRect(x: cvOrigin.x, y: cvOrigin.y, width: cvWidth, height: cellHeight )
aphoe
  • 1,996
  • 19
  • 21