77

I need to display a bunch of collectionViewCells that have different heights. the views are too complex and I don't want to manually calculate the expected height. I want to enforce auto-layout to calculate cell height

Calling dequeueReusableCellWithReuseIdentifier outside of cellForItemAtIndexPath breaks collectionView and causes it to crash

Another problem is the cell is not in a separate xib, so I can't manually instantiate a temporary one and use it for height calculation.

Any solutions for this?

public func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize { var cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath) as UICollectionViewCell configureCell(cell, item: items[indexPath.row]) cell.contentView.setNeedsLayout() cell.contentView.layoutIfNeeded() return cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize) } 

EDIT:

Crash happens as soon as dequeueReusableCellWithReuseIdentifier is called. If I don't call that method and instead return a size everything works great and cells show up without the calculated size

negative or zero sizes are not supported in the flow layout

2015-01-26 18:24:34.231 [13383:9752256] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 1 beyond bounds [0 .. 0]' *** First throw call stack: ( 0 CoreFoundation 0x00000001095aef35 __exceptionPreprocess + 165 1 libobjc.A.dylib 0x0000000109243bb7 objc_exception_throw + 45 2 CoreFoundation 0x0000000109499f33 -[__NSArrayM objectAtIndex:] + 227 3 UIKit 0x0000000107419d9c -[UICollectionViewFlowLayout _getSizingInfos] + 842 4 UIKit 0x000000010741aca9 -[UICollectionViewFlowLayout _fetchItemsInfoForRect:] + 526 5 UIKit 0x000000010741651f -[UICollectionViewFlowLayout prepareLayout] + 257 6 UIKit 0x000000010742da10 -[UICollectionViewData _prepareToLoadData] + 67 7 UIKit 0x00000001074301c6 -[UICollectionViewData layoutAttributesForItemAtIndexPath:] + 44 8 UIKit 0x00000001073fddb1 -[UICollectionView _dequeueReusableViewOfKind:withIdentifier:forIndexPath:viewCategory:] + 248 9 0x00000001042b824c _TFC1228BasePaginatingViewController14collectionViewfS0_FTCSo16UICollectionView6layoutCSo22UICollectionViewLayout22sizeForItemAtIndexPathCSo11NSIndexPath_VSC6CGSize + 700 10 0x00000001042b83d4 _TToFC1228BasePaginatingViewController14collectionViewfS0_FTCSo16UICollectionView6layoutCSo22UICollectionViewLayout22sizeForItemAtIndexPathCSo11NSIndexPath_VSC6CGSize + 100 11 UIKit 0x0000000107419e2e -[UICollectionViewFlowLayout _getSizingInfos] + 988 12 UIKit 0x000000010741aca9 -[UICollectionViewFlowLayout _fetchItemsInfoForRect:] + 526 13 UIKit 0x000000010741651f -[UICollectionViewFlowLayout prepareLayout] + 257 14 UIKit 0x000000010742da10 -[UICollectionViewData _prepareToLoadData] + 67 15 UIKit 0x000000010742e0e9 -[UICollectionViewData validateLayoutInRect:] + 54 16 UIKit 0x00000001073f67b8 -[UICollectionView layoutSubviews] + 170 17 UIKit 0x0000000106e3c973 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 521 18 QuartzCore 0x0000000106b0fde8 -[CALayer layoutSublayers] + 150 19 QuartzCore 0x0000000106b04a0e _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 380 20 QuartzCore 0x0000000106b0487e _ZN2CA5Layer28layout_and_display_if_neededEPNS_11TransactionE + 24 21 QuartzCore 0x0000000106a7263e _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 242 22 QuartzCore 0x0000000106a7374a _ZN2CA11Transaction6commitEv + 390 23 QuartzCore 0x0000000106a73db5 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv + 89 24 CoreFoundation 0x00000001094e3dc7 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23 25 CoreFoundation 0x00000001094e3d20 __CFRunLoopDoObservers + 368 26 CoreFoundation 0x00000001094d9b53 __CFRunLoopRun + 1123 27 CoreFoundation 0x00000001094d9486 CFRunLoopRunSpecific + 470 28 GraphicsServices 0x000000010be869f0 GSEventRunModal + 161 29 UIKit 0x0000000106dc3420 UIApplicationMain + 1282 30 0x000000010435c709 main + 169 31 libdyld.dylib 0x000000010a0f2145 start + 1 ) libc++abi.dylib: terminating with uncaught exception of type NSException 
3

10 Answers 10

29

Here is a Ray Wenderlich tutorial that shows you how to use AutoLayout to dynamically size UITableViewCells. I would think it would be the same for UICollectionViewCell.

Basically, though, you end up dequeueing and configuring a prototype cell and grabbing its height. After reading this article, I decided to NOT implement this method and just write some clear, explicit sizing code.

Here's what I consider the "secret sauce" for the entire article:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return [self heightForBasicCellAtIndexPath:indexPath]; } - (CGFloat)heightForBasicCellAtIndexPath:(NSIndexPath *)indexPath { static RWBasicCell *sizingCell = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sizingCell = [self.tableView dequeueReusableCellWithIdentifier:RWBasicCellIdentifier]; }); [self configureBasicCell:sizingCell atIndexPath:indexPath]; return [self calculateHeightForConfiguredSizingCell:sizingCell]; } - (CGFloat)calculateHeightForConfiguredSizingCell:(UITableViewCell *)sizingCell { [sizingCell setNeedsLayout]; [sizingCell layoutIfNeeded]; CGSize size = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; return size.height + 1.0f; // Add 1.0f for the cell separator height } 


EDIT: I did some research into your crash and decided that there is no way to get this done without a custom XIB. While that is a bit frustrating, you should be able to cut and paste from your Storyboard to a custom, empty XIB.

Once you've done that, code like the following will get you going:

// ViewController.m #import "ViewController.h" #import "CollectionViewCell.h" @interface ViewController () <UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout> { } @property (weak, nonatomic) IBOutlet CollectionViewCell *cell; @property (weak, nonatomic) IBOutlet UICollectionView *collectionView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor lightGrayColor]; [self.collectionView registerNib:[UINib nibWithNibName:@"CollectionViewCell" bundle:nil] forCellWithReuseIdentifier:@"cell"]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; NSLog(@"viewDidAppear..."); } - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { return 1; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return 50; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { return 10.0f; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { return 10.0f; } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { return [self sizingForRowAtIndexPath:indexPath]; } - (CGSize)sizingForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *title = @"This is a long title that will cause some wrapping to occur. This is a long title that will cause some wrapping to occur."; static NSString *subtitle = @"This is a long subtitle that will cause some wrapping to occur. This is a long subtitle that will cause some wrapping to occur."; static NSString *buttonTitle = @"This is a really long button title that will cause some wrapping to occur."; static CollectionViewCell *sizingCell = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sizingCell = [[NSBundle mainBundle] loadNibNamed:@"CollectionViewCell" owner:self options:nil][0]; }); [sizingCell configureWithTitle:title subtitle:[NSString stringWithFormat:@"%@: Number %d.", subtitle, (int)indexPath.row] buttonTitle:buttonTitle]; [sizingCell setNeedsLayout]; [sizingCell layoutIfNeeded]; CGSize cellSize = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; NSLog(@"cellSize: %@", NSStringFromCGSize(cellSize)); return cellSize; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { static NSString *title = @"This is a long title that will cause some wrapping to occur. This is a long title that will cause some wrapping to occur."; static NSString *subtitle = @"This is a long subtitle that will cause some wrapping to occur. This is a long subtitle that will cause some wrapping to occur."; static NSString *buttonTitle = @"This is a really long button title that will cause some wrapping to occur."; CollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath]; [cell configureWithTitle:title subtitle:[NSString stringWithFormat:@"%@: Number %d.", subtitle, (int)indexPath.row] buttonTitle:buttonTitle]; return cell; } @end 

The code above (along with a very basic UICollectionViewCell subclass and associated XIB) gives me this:

enter image description here

Sign up to request clarification or add additional context in comments.

15 Comments

This doesn't work. on collectionView it crashes.
... crashes where, with what sort of crash?
Thanks for the answer, that sucks that this is not possible to do
Well, it's not possible the way you want to do it (which, I agree, is a reasonable expectation), but it's still possible to use AutoLayout to dynamically size the cells. So, half a win, at least.
This is a great answer. Would you be willing to post this demo project on GitHub?
|
27

We can maintain dynamic height for collection view cell without xib(only using storyboard).

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { NSAttributedString* labelString = [[NSAttributedString alloc] initWithString:@"Your long string goes here" attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:17.0]}]; CGRect cellRect = [labelString boundingRectWithSize:CGSizeMake(cellWidth, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin context:nil]; return CGSizeMake(cellWidth, cellRect.size.height); } 

Make sure that numberOfLines in IB should be 0.

2 Comments

wish I could vote this up twice!
Such a simple solution! I made a static function in my collection view cell to perform this calculation and return the height.
27

I just ran into this problem on a UICollectionView and the way that i solved it similar to the answer above but in a pure UICollectionView way.

  1. Create a custom UICollectionViewCell that contains whatever you will be filling it with to make it dynamic. I created its own .xib for it as it seems like the easiest approach.

  2. Add constraints in that .xib that allow for the cell to be calculated from top to bottom. The re-sizing won't work if you haven't accounted for all of the height. Say you have a view on top, then a label underneath it, and another label underneath that. You would need to connect constraints to the top of the cell to the top of that view, then the bottom of the view to the top of the first label, bottom of first label to the top of the second label, and bottom of second label to bottom of cell.

  3. Load the .xib into the viewcontroller and register it with the collectionView on viewDidLoad

    let nib = UINib(nibName: CustomCellName, bundle: nil) self.collectionView!.registerNib(nib, forCellWithReuseIdentifier: "customCellID")` 
  4. Load a second copy of that xib into the class and store it as a property so you can use it to determine the size of what that cell should be

    let sizingNibNew = NSBundle.mainBundle().loadNibNamed(CustomCellName, owner: CustomCellName.self, options: nil) as NSArray self.sizingNibNew = (sizingNibNew.objectAtIndex(0) as? CustomViewCell)! 
  5. Implement the UICollectionViewFlowLayoutDelegate in your view controller. The method that matters is called sizeForItemAtIndexPath. Inside that method you will need to pull the data from the datasource that is associated with that cell from the indexPath. Then configure the sizingCell and call preferredLayoutSizeFittingSize. The method returns a CGSize which will consist of the width minus the content insets and the height that is returned from self.sizingCell.preferredLayoutSizeFittingSize(targetSize).

    override func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize { guard let data = datasourceArray?[indexPath.item] else { return CGSizeZero } let sectionInset = self.collectionView?.collectionViewLayout.sectionInset let widthToSubtract = sectionInset!.left + sectionInset!.right let requiredWidth = collectionView.bounds.size.width let targetSize = CGSize(width: requiredWidth, height: 0) sizingNibNew.configureCell(data as! CustomCellData, delegate: self) let adequateSize = self.sizingNibNew.preferredLayoutSizeFittingSize(targetSize) return CGSize(width: (self.collectionView?.bounds.width)! - widthToSubtract, height: adequateSize.height) } 
  6. In the class of the custom cell itself you will need to override awakeFromNib and tell the contentView that its size needs to be flexible

     override func awakeFromNib() { super.awakeFromNib() self.contentView.autoresizingMask = [UIViewAutoresizing.FlexibleHeight] } 
  7. In the custom cell override layoutSubviews

     override func layoutSubviews() { self.layoutIfNeeded() } 
  8. In the class of the custom cell implement preferredLayoutSizeFittingSize. This is where you will need to do any trickery on the items that are being laid out. If its a label you will need to tell it what its preferredMaxWidth should be.

    func preferredLayoutSizeFittingSize(_ targetSize: CGSize)-> CGSize { let originalFrame = self.frame let originalPreferredMaxLayoutWidth = self.label.preferredMaxLayoutWidth var frame = self.frame frame.size = targetSize self.frame = frame self.setNeedsLayout() self.layoutIfNeeded() self.label.preferredMaxLayoutWidth = self.questionLabel.bounds.size.width // calling this tells the cell to figure out a size for it based on the current items set let computedSize = self.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize) let newSize = CGSize(width:targetSize.width, height:computedSize.height) self.frame = originalFrame self.questionLabel.preferredMaxLayoutWidth = originalPreferredMaxLayoutWidth return newSize } 

All those steps should give you the correct sizes. If your getting 0 or other funky numbers than you haven't set up your constraints properly.

11 Comments

This is a great bit of code bolnad. Really helped me out. I had a cell with a stackpanel and two expanding labels inside - the cell now expands to fit the contents!
@JamesMundy thanks for the comment, I was hoping it would help clear it up for someone!
@bolnad : Bingo! It Worked. But I want to create a auto-expanding cell based on which expands/contracts on selection .. Any ideas?
@AbhishekBedi not sure what your asking, sounds like you want to create an insert cell when tapping on another cell? Kind of how apple does their inline picker in the calendar app?
Why is iOS so... painful? In android's ListView (or other recyclers) we just return views, that's all. Size must be determined by view, not by collection's view controller.
|
17

Swift 4.*

I have created a Xib for UICollectionViewCell which seems to be the good approach.

extension ViewController: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return size(indexPath: indexPath) } private func size(for indexPath: IndexPath) -> CGSize { // load cell from Xib let cell = Bundle.main.loadNibNamed("ACollectionViewCell", owner: self, options: nil)?.first as! ACollectionViewCell // configure cell with data in it let data = self.data[indexPath.item] cell.configure(withData: data) cell.setNeedsLayout() cell.layoutIfNeeded() // width that you want let width = collectionView.frame.width let height: CGFloat = 0 let targetSize = CGSize(width: width, height: height) // get size with width that you want and automatic height let size = cell.contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .defaultHigh, verticalFittingPriority: .fittingSizeLevel) // if you want height and width both to be dynamic use below // let size = cell.contentView.systemLayoutSizeFitting(UILayoutFittingCompressedSize) return size } } 

#note: I don't recommend setting image when configuring data in this size determining case. It gave me the distorted/unwanted result. Configuring texts only gave me below result.

enter image description here

Comments

13

TL;DR: Scan down to image, and then check out working project here.

Updating my answer for a simpler solution that I found..

In my case, I wanted to fix the width, and have variable height cells. I wanted a drop in, reusable solution that handled rotation and didn't require a lot of intervention.

What I arrived at, was override (just) systemLayoutFitting(...) in the collection cell (in this case a base class for me), and first defeat UICollectionView's effort to set the wrong dimension on contentView by adding a constraint for the known dimension, in this case, the width.

class EstimatedWidthCell: UICollectionViewCell { override init(frame: CGRect) { super.init(frame: frame) contentView.translatesAutoresizingMaskIntoConstraints = false } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) contentView.translatesAutoresizingMaskIntoConstraints = false } override func systemLayoutSizeFitting( _ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize { width.constant = targetSize.width 

and then return the final size for the cell - used for (and this feels like a bug) the dimension of the cell itself, but not contentView - which is otherwise constrained to a conflicting size (hence the constraint above). To calculate the correct cell size, I use a lower priority for the dimension that I wanted to float, and I get back the height required to fit the content within the width to which I want to fix:

 let size = contentView.systemLayoutSizeFitting( CGSize(width: targetSize.width, height: 1), withHorizontalFittingPriority: .required, verticalFittingPriority: verticalFittingPriority) print("\(#function) \(#line) \(targetSize) -> \(size)") return size } lazy var width: NSLayoutConstraint = { return contentView.widthAnchor .constraint(equalToConstant: bounds.size.width) .isActive(true) }() } 

But where does this width come from? It is configured via the estimatedItemSize on the collection view's flow layout:

lazy var collectionView: UICollectionView = { let view = UICollectionView(frame: CGRect(), collectionViewLayout: layout) view.backgroundColor = .cyan view.translatesAutoresizingMaskIntoConstraints = false return view }() lazy var layout: UICollectionViewFlowLayout = { let layout = UICollectionViewFlowLayout() let width = view.bounds.size.width // should adjust for inset layout.estimatedItemSize = CGSize(width: width, height: 10) layout.scrollDirection = .vertical return layout }() 

Finally, to handle rotation, I implement trailCollectionDidChange to invalidate the layout:

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { layout.estimatedItemSize = CGSize(width: view.bounds.size.width, height: 10) layout.invalidateLayout() super.traitCollectionDidChange(previousTraitCollection) } 

The final result looks like this:

enter image description here

And I have published a working sample here.

2 Comments

It seems to be the best answers as it doesn't need to use external Nib (.xib) for Collection View Cell. So this solution is most flexible. More over It is abstracted to UICollectionViewCell superclass that can be just inherited and reused. So you can make such AutomaticWidthCell, AutomaticHeightCell, AutomaticSizeCell, and only set estimatedItemSize on CollectionView Layout. I have used this with StackViews in UICollectionViewCell, so hidding some elements easily collapses Cell layout
Nice solution but I just wanted to say that you shouldn't be handling rotation in trailCollectionDidChange, it gets called for the change of any trait and you may be invalidating the layout when it's not needed. viewWillTransition is the right place.
5

Seems like it's quite a popular question, so I will try to make my humble contribution.


The code below is Swift 4 solution for no-storyboard setup. It utilizes some approaches from previous answers, therefore it prevents Auto Layout warning caused on device rotation.

I am sorry if code samples are a bit long. I want to provide an "easy-to-use" solution fully hosted by StackOverflow. If you have any suggestions to the post - please, share the idea and I will update it accordingly.

The setup:

Two classes: ViewController.swift and MultilineLabelCell.swift - Cell containing single UILabel.

MultilineLabelCell.swift

import UIKit class MultilineLabelCell: UICollectionViewCell { static let reuseId = "MultilineLabelCellReuseId" private let label: UILabel = UILabel(frame: .zero) override init(frame: CGRect) { super.init(frame: frame) layer.borderColor = UIColor.red.cgColor layer.borderWidth = 1.0 label.numberOfLines = 0 label.lineBreakMode = .byWordWrapping let labelInset = UIEdgeInsets(top: 10, left: 10, bottom: -10, right: -10) contentView.addSubview(label) label.translatesAutoresizingMaskIntoConstraints = false label.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor, constant: labelInset.top).isActive = true label.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor, constant: labelInset.left).isActive = true label.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor, constant: labelInset.right).isActive = true label.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor, constant: labelInset.bottom).isActive = true label.layer.borderColor = UIColor.black.cgColor label.layer.borderWidth = 1.0 } required init?(coder aDecoder: NSCoder) { fatalError("Storyboards are quicker, easier, more seductive. Not stronger then Code.") } func configure(text: String?) { label.text = text } override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { label.preferredMaxLayoutWidth = layoutAttributes.size.width - contentView.layoutMargins.left - contentView.layoutMargins.left layoutAttributes.bounds.size.height = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height return layoutAttributes } } 

ViewController.swift

import UIKit let samuelQuotes = [ "Samuel says", "Add different length strings here for better testing" ] class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { private(set) var collectionView: UICollectionView // Initializers init() { // Create new `UICollectionView` and set `UICollectionViewFlowLayout` as its layout collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { // Create new `UICollectionView` and set `UICollectionViewFlowLayout` as its layout collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) super.init(coder: aDecoder) } override func viewDidLoad() { super.viewDidLoad() title = "Dynamic size sample" // Register Cells collectionView.register(MultilineLabelCell.self, forCellWithReuseIdentifier: MultilineLabelCell.reuseId) // Add `coolectionView` to display hierarchy and setup its appearance view.addSubview(collectionView) collectionView.backgroundColor = .white collectionView.contentInsetAdjustmentBehavior = .always collectionView.contentInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) // Setup Autolayout constraints collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true collectionView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true collectionView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true collectionView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true // Setup `dataSource` and `delegate` collectionView.dataSource = self collectionView.delegate = self (collectionView.collectionViewLayout as! UICollectionViewFlowLayout).estimatedItemSize = UICollectionViewFlowLayout.automaticSize (collectionView.collectionViewLayout as! UICollectionViewFlowLayout).sectionInsetReference = .fromLayoutMargins } // MARK: - UICollectionViewDataSource - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MultilineLabelCell.reuseId, for: indexPath) as! MultilineLabelCell cell.configure(text: samuelQuotes[indexPath.row]) return cell } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return samuelQuotes.count } // MARK: - UICollectionViewDelegateFlowLayout - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let sectionInset = (collectionViewLayout as! UICollectionViewFlowLayout).sectionInset let referenceHeight: CGFloat = 100 // Approximate height of your cell let referenceWidth = collectionView.safeAreaLayoutGuide.layoutFrame.width - sectionInset.left - sectionInset.right - collectionView.contentInset.left - collectionView.contentInset.right return CGSize(width: referenceWidth, height: referenceHeight) } } 

To run this sample create new Xcode project, create corresponding files and replace AppDelegate contents with the following code:

import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? var navigationController: UINavigationController? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) if let window = window { let vc = ViewController() navigationController = UINavigationController(rootViewController: vc) window.rootViewController = navigationController window.makeKeyAndVisible() } return true } } 

Comments

4

Swift 4 answer based on helpful answer from @mbm29414.

Unfortunately, it requires the use of a XIB file. There doesn't appear to be an alternative.

The key parts are using a sizing cell (created only once) and registering the XIB when initializing the collection view.

Then you size each cell dynamically within the sizeForItemAt function.

// UICollectionView Vars and Constants let CellXIBName = YouViewCell.XIBName let CellReuseID = YouViewCell.ReuseID var sizingCell = YouViewCell() fileprivate func initCollectionView() { // Connect to view controller collectionView.dataSource = self collectionView.delegate = self // Register XIB collectionView.register(UINib(nibName: CellXIBName, bundle: nil), forCellWithReuseIdentifier: CellReuseID) // Create sizing cell for dynamically sizing cells sizingCell = Bundle.main.loadNibNamed(CellXIBName, owner: self, options: nil)?.first as! YourViewCell // Set scroll direction let layout = UICollectionViewFlowLayout() layout.scrollDirection = .vertical collectionView.collectionViewLayout = layout // Set properties collectionView.alwaysBounceVertical = true collectionView.alwaysBounceHorizontal = false // Set top/bottom padding collectionView.contentInset = UIEdgeInsets(top: collectionViewTopPadding, left: collectionViewSidePadding, bottom: collectionViewBottomPadding, right: collectionViewSidePadding) // Hide scrollers collectionView.showsVerticalScrollIndicator = false collectionView.showsHorizontalScrollIndicator = false } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { // Get cell data and render post let data = YourData[indexPath.row] sizingCell.renderCell(data: data) // Get cell size sizingCell.setNeedsLayout() sizingCell.layoutIfNeeded() let cellSize = sizingCell.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) // Return cell size return cellSize } 

1 Comment

Let's see if I got this right. YouViewCell is not your actual cell class or actual cell Xib? It is specifically created to deal with sizing? Or... do I have to pull my actual cell designs out of my Storyboard and put them in separate Xibs?
1

I followed the steps mentioned in this SO and everything is fine except when my Collection View has less data (text) to make it wide enough. Checking the documentation in systemLyaoutSizeFittingSize, I have this solution so my cell take up the width as I requested:

- (CGSize)calculateSizeForSizingCell:(UICollectionViewCell *)sizingCell width:(CGFloat)width { CGRect frame = sizingCell.frame; frame.size.width = width; sizingCell.frame = frame; [sizingCell setNeedsLayout]; [sizingCell layoutIfNeeded]; CGSize size = [sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel]; return size; } 

Hope this would help someone.

- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize NS_AVAILABLE_IOS(6_0); 

Apple doc:

Equivalent to sending -systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority: with UILayoutPriorityFittingSizeLevel for both priorities.

While the default value is "pretty low" according to Apple's doc:

When you send -[UIView systemLayoutSizeFittingSize:], the size fitting most closely to the target size (the argument) is computed. UILayoutPriorityFittingSizeLevel is the priority level with which the view wants to conform to the target size in that computation. It's quite low. It is generally not appropriate to make a constraint at exactly this priority. You want to be higher or lower.

So my change of default behavior is to enforce the width (horizontal fitting) with UILayoutPriorityRequired.

Comments

1

Follow bolnad answer up to Step 4.

Then make it simpler by replacing all the other steps with:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { // Configure your cell sizingNibNew.configureCell(data as! CustomCellData, delegate: self) // We use the full width minus insets let width = collectionView.frame.size.width - collectionView.sectionInset.left - collectionView.sectionInset.right // Constrain our cell to this width let height = sizingNibNew.systemLayoutSizeFitting(CGSize(width: width, height: .infinity), withHorizontalFittingPriority: UILayoutPriorityRequired, verticalFittingPriority: UILayoutPriorityFittingSizeLevel).height return CGSize(width: width, height: height) } 

Comments

1

It worked for me, hope you too.

*Note: I have used auto layout in Nib, remember add top and bottom contraints for subviews in contentView

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let cell = YourCollectionViewCell.instantiateFromNib() cell.frame.size.width = collectionView.frame.width cell.data = viewModel.data[indexPath.item] let resizing = cell.systemLayoutSizeFitting(UILayoutFittingCompressedSize, withHorizontalFittingPriority: UILayoutPriority.required, verticalFittingPriority: UILayoutPriority.fittingSizeLevel) return resizing } 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.