UICollectionView - dynamic cell height? [duplicate]
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
Solution 1:
Here is a Ray Wenderlich tutorial that shows you how to use AutoLayout to dynamically size UITableViewCell
s. 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:
Solution 2:
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 calledsizeForItemAtIndexPath
. 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 callpreferredLayoutSizeFittingSize
. The method returns a CGSize which will consist of the width minus the content insets and the height that is returned fromself.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 thecontentView
that its size needs to be flexibleoverride 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.
Solution 3:
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.