动态单元格高度 UICollectionView? ?

我需要显示一组具有不同高度的 CollectionViewCell。视图太复杂了,我不想手动计算预期的高度。我想实施自动布局来计算单元格高度

cellForItemAtIndexPath之外调用 dequeueReusableCellWithReuseIdentifier会破坏 CollectionView 并导致其崩溃

另一个问题是单元格不在一个单独的 xib 中,所以我不能手动实例化一个临时单元格并使用它来计算高度。

有什么解决办法吗?

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

编辑:

一旦调用 dequeueReusableCellWithReuseIdentifier,就会发生崩溃。如果我不调用这个方法,而是返回一个大小,那么一切都很好,单元格显示出来时没有计算出的大小

流程布局中不支持负值或零值大小

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
142479 次浏览

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

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.

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.

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.

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

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.

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

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
}

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

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
}