UICollectionViewFlowLayout issues - ios

Apologies in advance but I've been going around in circles on this for days...
I have a UIViewController that is presented from another VC (in my case, a button is tapped). The new VC (code below) is made up of:
a UITextView which dynamically increases in height based on content the user types in
a UIView which has a fixed height
and then below that, to the bottom of the ViewController view, is a UICollectionView. This has 5 sections in it that horizontally scroll. I will be inserting different content into each of those 5 cells but am just having issues at the moment when the UICollectionView resizes. I've been able to get most of them cleared except for one.
This error can be reproduced whenever the UITextView increases in size to between 3-6 lines long, and again at 13 lines... This happens in the simulator using e.g. an iPhone 8 plus but on an iPhone 11 Pro Max, it only happens around 19 lines of text in the UITextView.
I am using TinyContraints here but I've tested using traditional programmatic constraints and had the same issue.
//
// NewItemVC.swift
//
import TinyConstraints
import UIKit
class NewItemVC: UIViewController {
let titleField = UITextView()
let bannerContainer = UIView(frame: .zero)
var flowLayout = UICollectionViewFlowLayout()
lazy var horizontalCV = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
let genericCellID = "genericCellID"
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .orange
setupHeaderArea()
setupBannerArea()
setupHorizontalCollectionView()
}
func setupHeaderArea(){
titleField.backgroundColor = .lightGray
titleField.textContainerInset.top = 55
titleField.textContainerInset.bottom = 20
titleField.textContainerInset.left = 15
titleField.textContainerInset.right = 15
titleField.font = UIFont.boldSystemFont(ofSize: 20)
// add to main view & position
view.addSubview(titleField)
titleField.topToSuperview()
titleField.leftToSuperview()
titleField.rightToSuperview()
titleField.isScrollEnabled = false
NotificationCenter.default.addObserver(self, selector: #selector(updateTextFieldHeight), name: UITextView.textDidChangeNotification, object: nil)
// add a header label
let headerLabel = UILabel()
headerLabel.textColor = .systemBlue
headerLabel.text = "NEW ITEM"
headerLabel.font = UIFont.systemFont(ofSize: 12)
view.addSubview(headerLabel)
headerLabel.topToSuperview(offset: 35)
headerLabel.leftToSuperview(offset: 20)
headerLabel.height(13)
// add card handle
let handle = UIView()
handle.backgroundColor = .black
handle.alpha = 0.2
handle.width(45)
handle.height(5)
handle.layer.cornerRadius = 5/2
view.addSubview(handle)
handle.topToSuperview(offset: 10)
handle.centerXToSuperview()
}
func setupBannerArea(){
// position container
view.addSubview(bannerContainer)
bannerContainer.topToBottom(of: titleField)
bannerContainer.edgesToSuperview(excluding: [.top, .bottom])
bannerContainer.height(62)
bannerContainer.backgroundColor = .cyan
}
func setupHorizontalCollectionView() {
horizontalCV = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
horizontalCV.dataSource = self
horizontalCV.delegate = self
horizontalCV.backgroundColor = .yellow
horizontalCV.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
horizontalCV.scrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
view.addSubview(horizontalCV)
horizontalCV.topToBottom(of: bannerContainer)
horizontalCV.edgesToSuperview(excluding: .top)
horizontalCV.isPagingEnabled = true
horizontalCV.contentInsetAdjustmentBehavior = .never
flowLayout.scrollDirection = .horizontal
flowLayout.minimumLineSpacing = 0
flowLayout.minimumInteritemSpacing = 0
horizontalCV.register(UICollectionViewCell.self, forCellWithReuseIdentifier: genericCellID)
}
#objc func updateTextFieldHeight() {
DispatchQueue.main.async{
self.horizontalCV.collectionViewLayout.invalidateLayout()
}
}
}
// MARK:- Delegates - UITextViewDelegate
extension NewItemVC: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
let size = CGSize(width: textView.frame.width, height: .infinity)
let estimatedSize = textView.sizeThatFits(size)
textView.constraints.forEach { (constraint) in
if constraint.firstAttribute == .height {
constraint.constant = estimatedSize.height
}
}
}
}
// MARK:- UICollectionView Data Source
extension NewItemVC: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: genericCellID, for: indexPath)
if indexPath.row % 2 == 0 {
cell.backgroundColor = .orange
} else {
cell.backgroundColor = .red
}
return cell
}
}
// MARK:- Delegates - UICollectionView Flow Layout
extension NewItemVC: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let requiredHeight = self.view.frame.height - (titleField.frame.height + bannerContainer.frame.height)
let requiredWidth = view.frame.width
return CGSize(width: requiredWidth, height: requiredHeight)
}
}
From what I've been able to tell, there's some issue randomly where the collectionView frame and the contentSize are out by 24 points but I can't work out why/where etc...
The error is as follows:
2020-02-06 20:39:18.425280+1100 Scrolling[67237:8005204] The behavior of the UICollectionViewFlowLayout is not defined because:
2020-02-06 20:39:18.425378+1100 Scrolling[67237:8005204] the item height must be less than the height of the UICollectionView minus the section insets top and bottom values, minus the content insets top and bottom values.
2020-02-06 20:39:18.425672+1100 Scrolling[67237:8005204] The relevant UICollectionViewFlowLayout instance is <UICollectionViewFlowLayout: 0x7fcf56d4a120>, and it is attached to <UICollectionView: 0x7fcf5785d000; frame = (0 208.667; 414 487.333); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x600000ce8a50>; layer = <CALayer: 0x600000213a00>; contentOffset: {0, 0}; contentSize: {2070, 511}; adjustedContentInset: {0, 0, 0, 0}; layout: <UICollectionViewFlowLayout: 0x7fcf56d4a120>; dataSource: <Scrolling.NewItemVC: 0x7fcf56d20110>>.
2020-02-06 20:39:18.425760+1100 Scrolling[67237:8005204] Make a symbolic breakpoint at UICollectionViewFlowLayoutBreakForInvalidSizes to catch this in the debugger.

Related

Set width of custom UIView nib to it's parent CollectionViewCell

This is what I have:
A Collection View with 2 columns with each an equal distance apart.
Each cell loads SmallCardView.xib. The SmallCardView contains a square image with some text below.
The problem:
I want the width of the view to match that of it's parent (the cell). This is best illustrated by comparing screen sizes
As you can see above, the cell (purple outline) sizes correctly on both screens but the SmallCardView remains the same size
Here is the code in my Collection View Controller:
viewDidLoad -
private let spacing: CGFloat = 20.0
override func viewDidLoad() {
super.viewDidLoad()
let layout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: spacing, left: spacing, bottom: spacing, right: spacing)
self.collectionView?.collectionViewLayout = layout
}
sizeForItemAt -
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let numberOfItemsPerRow: CGFloat = 2
let spacingBetweenCells: CGFloat = 20
let totalSpacing = (2 * self.spacing) + ((numberOfItemsPerRow - 1) * spacingBetweenCells) // Amount of total spacing in a row
if let collection = self.collectionView {
let width = (collection.bounds.width - totalSpacing)/numberOfItemsPerRow
return CGSize(width: width, height: width * 1.2)
} else {
return CGSize(width: 0, height: 0)
}
}
Thanks!
You can embed view with constraints to edges as:
extension UIView {
func makeEdges(to view: UIView, useMargins: Bool = false) -> [NSLayoutConstraint] {
return [
(useMargins ? layoutMarginsGuide.leftAnchor : leftAnchor).constraint(equalTo: view.leftAnchor),
(useMargins ? layoutMarginsGuide.rightAnchor : rightAnchor).constraint(equalTo: view.rightAnchor),
(useMargins ? layoutMarginsGuide.topAnchor : topAnchor).constraint(equalTo: view.topAnchor),
(useMargins ? layoutMarginsGuide.bottomAnchor : bottomAnchor).constraint(equalTo: view.bottomAnchor)
]
}
func edges(to view: UIView, useMargins: Bool = false) {
NSLayoutConstraint.activate(makeEdges(to: view, useMargins: useMargins))
}
}
And use it as:
let cardView = ...
cardView.translatesAutoresizingMaskIntoConstraints = false
let cell = ...
cell.contentView.addSubview(cardView)
cell.contentView.edges(to: cardView, useMargins: true)
First take collectionview from storyboard. Set it's constraints like as below :-
Leading space to container - 20
Trailing space to container - 20
Top space to container - 20
Bottom space to container - 20
Then select collectionview and remove lines padding that is default 10. So, update with 20. So, cellpadding should be 10 each side.
Then Go to the viewcontroller file and add all 3 delegates
1. UICollectionViewDelegate
2. UICollectionViewDataSource
3. UICollectionViewDelegateFlowLayout
Then add following code in your swift file :-
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: (self.view.frame.size.width - 60) / 2, height: (self.view.frame.size.width - 60) / 2)
}

UICollectionView height with auto layout cells [duplicate]

This question already has answers here:
UICollectionView, full width cells, allow autolayout dynamic height?
(21 answers)
Closed 3 years ago.
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
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:
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.
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.
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.
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")`
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)!
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)
}
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]
}
In the custom cell override layoutSubviews
override func layoutSubviews() {
self.layoutIfNeeded()
}
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.
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.
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:
And I have published a working sample here.
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
}
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)
}
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
}

Equal horizontal spacing UICollectionView, including insets - UICollectionViewFlowLayout?

I'm trying to create equal horizontal spacing between two UICollectionViewCell and between a UICollectionViewCell an the screen border. It should look like this.
I think it is possible to use UICollectionViewFlowLayout to adjust the width of every UICollectionViewCell, so they have equal spacings regardless of the phone's size. There should always only be 3 cells on each row.
First of all you should add constants for all parameters of UICollectionView to the controller:
let itemsPerRow = 3
let spaceBetweenItems: CGFloat = 20.0
let spaceBetweenLines: CGFloat = 40.0
let itemHeight: CGFloat = 50.0
You mentioned that item's width should be adjustable. You can calculate it with this function:
var itemWidth: CGFloat {
return (self.collectionView.frame.width - CGFloat(self.itemsPerRow + 1) * self.spaceBetweenItems) / CGFloat(self.itemsPerRow)
}
Of course you need to customise layout of your UICollectionView instance:
var collectionViewLayout: UICollectionViewFlowLayout {
let result = UICollectionViewFlowLayout()
result.itemSize = CGSize(width: self.itemWidth, height: self.itemHeight)
result.minimumInteritemSpacing = self.spaceBetweenItems
result.minimumLineSpacing = self.spaceBetweenLines
result.sectionInset = UIEdgeInsets(top: 0.0, left: self.spaceBetweenItems, bottom: 0.0, right: self.spaceBetweenItems)
result.scrollDirection = .vertical
return result
}
And in the function viewDidLoad or your controller you should call setupCollectionView(). Here is a code of this function:
func setupCollectionView() {
self.collectionView.frame = self.view.frame
self.collectionView.collectionViewLayout = self.collectionViewLayout
}
You will see something like this:

UICollectionView edge to edge layout

Goal: edge-to-edge UICollectionView with 2 cells on all size iPhones.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let screenSize = collectionView.bounds
let screenWidth = screenSize.width
print("Zhenya: \(screenWidth)") // prints correct 375, which is half of iPhone 6
let cellEdgeLength: CGFloat = screenWidth / 2.0
return CGSize(width: cellEdgeLength, height: cellEdgeLength)
}
Also
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
let flow = UICollectionViewFlowLayout()
flow.scrollDirection = UICollectionViewScrollDirection.vertical
flow.minimumInteritemSpacing = 0
flow.minimumLineSpacing = 0
collectionView.collectionViewLayout = flow
}
However on iPhone 6:
Collection Cell attributes:
Collection View attributes:
Update:
func for gradient, that actually gets the right width:
(located at custom UICollectionViewCell class)
func addGradient () {
let gradient = CAGradientLayer()
gradient.frame = gradientView.bounds
let topColor = UIColor(red:0.07, green:0.07, blue:0.07, alpha:1)
let botomColor = UIColor.clear
gradient.colors = [topColor.cgColor, botomColor.cgColor]
if gradientWasRemoved == false {
gradientView.layer.insertSublayer(gradient, at: 0)
} else if gradientWasRemoved == true {
self.addSubview(gradientView)
}
Update 2:
Note: Testing on iPhone 7 Plus.
I found that UICollectionViewFlowLayout overrides cell size crated in sizeForItemAtIndexPath: (seems like it)
With this code:
let flow = UICollectionViewFlowLayout()
let screenSize = collectionView.bounds
let screenWidth = screenSize.width
let cellEdgeLength: CGFloat = screenWidth / 2.0
flow.itemSize = CGSize(width: cellEdgeLength, height: cellEdgeLength)
flow.scrollDirection = UICollectionViewScrollDirection.vertical
flow.minimumInteritemSpacing = 0
flow.minimumLineSpacing = 0
collectionView.collectionViewLayout = flow
I have this:
Then I decided manual specify cell edge length (half of iPhone7Plust width = 207):
let cellSide: CGFloat = 207
flow.itemSize = CGSize(width: cellSide, height: cellSide)
flow.scrollDirection = UICollectionViewScrollDirection.vertical
flow.minimumInteritemSpacing = 0
flow.minimumLineSpacing = 0
collectionView.collectionViewLayout = flow
I get this:
Found the solution.
Problem was in the inner imageView autolayout settings. I didn't specify them.
Why that little piece of BS was getting on the way of my flow.itemSize, I don't know.
What is more weird to me, is why when manually specified cellEdgeLength as 207 in the second update, all of the sudden, it did override the lack of imageView constraints.

UICollectionView - dynamic cell height? [duplicate]

This question already has answers here:
UICollectionView, full width cells, allow autolayout dynamic height?
(21 answers)
Closed 3 years ago.
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
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:
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.
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.
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.
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")`
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)!
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)
}
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]
}
In the custom cell override layoutSubviews
override func layoutSubviews() {
self.layoutIfNeeded()
}
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.
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.
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:
And I have published a working sample here.
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
}
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)
}
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
}

Resources