UICollectionView - dynamic cell height?

aryaxt picture aryaxt · Jan 27, 2015 · Viewed 105.9k times · Source

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

Answer

mbm29414 picture mbm29414 · Jan 27, 2015

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