How to Determine Height of Uicollectionview with Flowlayout

How to determine height of UICollectionView with FlowLayout

Whoa! For some reason, after hours of research, I now found a pretty easy answer to my question: I was completely searching in the wrong place, digging through all the documentation I could find on UICollectionView.

The simple and easy solution lies in the underlying layout: Just call collectionViewContentSize on your myCollectionView.collectionViewLayout property and you get the height and width of the content as CGSize. It's as easy as that.

How to set UICollectionViewCell Width and Height programmatically

Use this method to set custom cell height width.

Make sure to add this protocols

UICollectionViewDelegate

UICollectionViewDataSource

UICollectionViewDelegateFlowLayout

If you are using swift 5 or xcode 11 and later you need to set Estimate Size to none using storyboard in order to make it work properly. If you will not set that than below code will not work as expected.

Sample Image

Swift 4 or Later

extension YourViewController: UICollectionViewDelegate {
//Write Delegate Code Here
}

extension YourViewController: UICollectionViewDataSource {
//Write DataSource Code Here
}

extension YourViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: screenWidth, height: screenWidth)
}
}

Objective-C

@interface YourViewController : UIViewController<UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout>

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return CGSizeMake(CGRectGetWidth(collectionView.frame), (CGRectGetHeight(collectionView.frame)));
}

How to adjust height of UICollectionView to be the height of the content size of the UICollectionView?

I would suggest the following:

  1. Add a height constraint to your collection view.
  2. Set its priority to 999.
  3. Set its constant to any value that makes it reasonably visible on the storyboard.
  4. Change the bottom equal constraint of the collection view to greater or equal.
  5. Connect the height constraint to an outlet.
  6. Every time you reload the data on the collection view do the following:

You may also want to consider the Inset of the collection view by adding it to the content size.

Code Sample:

CGFloat height = myCollectionView.collectionViewLayout.collectionViewContentSize.height
heightConstraint.constant = height
self.view.setNeedsLayout() Or self.view.layoutIfNeeded()

Explanation: Extra, You don't have to read if you understand it. obviously!!

The UI will try to reflect all the constraints no matter what are their priorities. Since there is a height constraint with lower priority of (999), and a bottom constraint of type greater or equal. whenever, the height constraint constant is set to a value less than the parent view height the collection view will be equal to the given height, achieving both constraints.

But, when the height constraint constant set to a value more than the parent view height both constraints can't be achieved. Therefore, only the constraint with the higher priority will be achieved which is the greater or equal bottom constraint.

The following is just a guess from an experience. So, it achieves one constrant. But, it also tries to make the error in the resulted UI for the other un-achieved lower priority constraint as lowest as possible. Therefore, the collection view height will be equal to the parent view size.

UICollectionView autosize height

I solved this eventually by fixing all Auto Layout issues, fixing the height of the collection view using a constraint. Then, whenever I know the content has changed I update the value of the constraint using the value collectionView.contentSize.height:

self.verticalLayoutConstraint.constant = self.collectionView.contentSize.height;

Then the collection view is resized properly and it behaves nicely within the overall scrollview. I have updated the GitHub test project with my changes.

To me, doing this by updating the constraint manually instead of being able to tell iOS: "make the frame height of the collection view as large as needed" does not feel right to me, but it's the best I have come up with so far. Please post a better answer if you have one.

UICollectionView Self Sizing Cells with Auto Layout


This answer is outdated from iOS 14 with the addition of compositional layouts. Please consider updating the new API

Updated for Swift 5

preferredLayoutAttributesFittingAttributes renamed to preferredLayoutAttributesFitting and use auto sizing



Updated for Swift 4

systemLayoutSizeFittingSize renamed to systemLayoutSizeFitting



Updated for iOS 9

After seeing my GitHub solution break under iOS 9 I finally got the time to investigate the issue fully. I have now updated the repo to include several examples of different configurations for self sizing cells. My conclusion is that self sizing cells are great in theory but messy in practice. A word of caution when proceeding with self sizing cells.

TL;DR

Check out my GitHub project


Self sizing cells are only supported with flow layout so make sure thats what you are using.

There are two things you need to setup for self sizing cells to work.

#1. Set estimatedItemSize on UICollectionViewFlowLayout

Flow layout will become dynamic in nature once you set the estimatedItemSize property.

self.flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

#2. Add support for sizing on your cell subclass

This comes in 2 flavours; Auto-Layout or custom override of preferredLayoutAttributesFittingAttributes.

Create and configure cells with Auto Layout

I won't go to in to detail about this as there's a brilliant SO post about configuring constraints for a cell. Just be wary that Xcode 6 broke a bunch of stuff with iOS 7 so, if you support iOS 7, you will need to do stuff like ensure the autoresizingMask is set on the cell's contentView and that the contentView's bounds is set as the cell's bounds when the cell is loaded (i.e. awakeFromNib).

Things you do need to be aware of is that your cell needs to be more seriously constrained than a Table View Cell. For instance, if you want your width to be dynamic then your cell needs a height constraint. Likewise, if you want the height to be dynamic then you will need a width constraint to your cell.

Implement preferredLayoutAttributesFittingAttributes in your custom cell

When this function is called your view has already been configured with content (i.e. cellForItem has been called). Assuming your constraints have been appropriately set you could have an implementation like this:

//forces the system to do one layout pass
var isHeightCalculated: Bool = false

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
//Exhibit A - We need to cache our calculation to prevent a crash.
if !isHeightCalculated {
setNeedsLayout()
layoutIfNeeded()
let size = contentView.systemLayoutSizeFitting(layoutAttributes.size)
var newFrame = layoutAttributes.frame
newFrame.size.width = CGFloat(ceilf(Float(size.width)))
layoutAttributes.frame = newFrame
isHeightCalculated = true
}
return layoutAttributes
}

NOTE On iOS 9 the behaviour changed a bit that could cause crashes on your implementation if you are not careful (See more here). When you implement preferredLayoutAttributesFittingAttributes you need to ensure that you only change the frame of your layout attributes once. If you don't do this the layout will call your implementation indefinitely and eventually crash. One solution is to cache the calculated size in your cell and invalidate this anytime you reuse the cell or change its content as I have done with the isHeightCalculated property.

Experience your layout

At this point you should have 'functioning' dynamic cells in your collectionView. I haven't yet found the out-of-the box solution sufficient during my tests so feel free to comment if you have. It still feels like UITableView wins the battle for dynamic sizing IMHO.

##Caveats
Be very mindful that if you are using prototype cells to calculate the estimatedItemSize - this will break if your XIB uses size classes. The reason for this is that when you load your cell from a XIB its size class will be configured with Undefined. This will only be broken on iOS 8 and up since on iOS 7 the size class will be loaded based on the device (iPad = Regular-Any, iPhone = Compact-Any). You can either set the estimatedItemSize without loading the XIB, or you can load the cell from the XIB, add it to the collectionView (this will set the traitCollection), perform the layout, and then remove it from the superview. Alternatively you could also make your cell override the traitCollection getter and return the appropriate traits. It's up to you.

Let me know if I missed anything, hope I helped and good luck coding



UICollectionView - dynamic cell height?

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:

Sample Image



Related Topics



Leave a reply



Submit