How can I change the size of a UICollectionViewCell based on the size of its subviews, using Auto Layout?
My UICollectionViewCells each only have one view in their contentView. This is a custom UIView subclass of mine that automatically resizes itself to fit all its subviews using Auto Layout. It also implements -intrinsicContentSize correctly. However the UICollectionView, or rather its layout, do not seem to care about this.
I am using a UICollectionViewFlowLayout, and tried:
Leaving the itemSize at its default value. This gives me cells with a size of 50 x 50.
Implementing the delegate method -collectionView:layout:sizeForItemAtIndexPath:. However, when this method gets called, the cells (obviously) have not appeared on screen yet, and have not been updated by the Auto Layout system.
Is there any way to change my cells' size based on their subviews using the standard Apple classes? Or do I have to implement my own UICollectionViewLayout subclass, or something else?
Have you tried this :
[self.view layoutIfNeeded];
[UIView animateWithDuration:0.5 animations:^{
constraintConstantForWidth.constant = subviews.size.width;
constraintConstantForHeight.constant = subviews.size.height;
[self.view layoutIfNeeded];
}];
and make the priority of the constraint (low = 250)
I think that this issue we can handle by the constraint constant replacement.
I used
- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row == 0) {
UIButton *button = (UIButton *)[cell viewWithTag:2];
CGRect frame = cell.frame;
frame.size.width = button.frame.size.width;
cell.frame = frame;
}
}
and was able to resize my cell based on the width of the button subview in a little test app.
Make sure that the tags on your subviews are greater than 0 because if you use 0, it just gives you back the contentView. Other than that, this should get you there
Try collectionView:layout:sizeForItemAtIndexPath:
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return self.view.frame.size;
}
Related
Inside the reusable view of my cell, I have a UIView.
Then, I have this method in the controller
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
// .. set value for CGFloat backgroundHeight
[cell addSubview:cell.backgroundView];
CGRect f = cell.backgroundView.frame;
f.size.height = backgroundHeight;
cell.backgroundView.frame = f;
}
But the UIView's height remains the same as specified in the Layout Rectangle.
What should I try next?
Your problem here lies in the fact that you are attempting to use the cell's backgroundView.
Firstly, you cannot add the cell's backgroundView as a subview. You simply assign a UIView to it with :
cell.backgroundView = yourView;
Secondly, if you read the docs, it clearly states :
Use this property to assign a custom background view to the cell. The background view is placed behind the content view and its frame is automatically adjusted so that it fills the bounds of the cell.
This means, no matter what frame you try to set for the backgroundView it will automatically adjust and fill the entire cell. Now, I haven't actually tried it, but you might be able to override this by subclassing. Though, i'll mention here, I am unsure.
Back to your problem, if you really want a UIView that you can control, you will need to create a UIView and then add it as a subview. Using the cell's backgroundView is not the solution.
It just seems like useless, what you'r approaching with the UICollectionViewCell's backgroundView .
By the Doc
backgroundView The view that provides the background appearance.
#property (nonatomic, retain) UIView *backgroundView; Discussion The
view (if any) in this property is positioned underneath all of the
other content and sized automatically to fill the entire bounds of the
collection view. The background view does not scroll with the
collection view’s other content. The collection view maintains a
strong reference to the background view object.
This property is nil by default, which displays the background color
of the collection view.
the backgroundView is just nothing but the cell, so what you'r upto do is doesn't effect . seems like directly changing the Cell's height.
the best solution is to just ignore the backgroundView property all
together. Instead, make the collection view’s background clear, and
implement your own View; just throw a view behind the
collection view.
Kindly check this blog, this would be helpful for you.
You can manage the Layout height With sizeForItemAtIndexPath
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath{
return CGSizeMake(view.frame.size.width, view.frame.size.height);
}
here you can manage spacing
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
return UIEdgeInsetsMake(5,5,0,5);
}
Controlling UICollectionViewCells is just the same as UITableViewCell. What you need to do is create a UICollectionViewCell subclass. These can seem confusing to start with but are really pretty simple to set up.
The first thing is to add any additional properties you might need like additional UIImageViews, UILabels etc. Now, we need to make sure all objects are instantiated but only once so this happens in
- (id)initWithFrame:(CGRect)frame
As well as instantiating an adding subviews to self.contentView set any global or default properties such as font, color etc. You can't set a frame relative to self.contentView yet though because self.contentView has zero size until layoutSubviews.
Now, create a method:
-(void)layoutSubviews
{
[super layout subviews];
...
[self.backgroundView setFrame: myFrame]; // in this case
}
The [super layoutSubviews] is important to set self.contentView's frame from the delegate cell layout methods. This method is called every time the cell comes into view or changes in any way (which is often). What you need to do now is set the various frames of things based on self.contentView.frame or self.frame. Also, you can set any conditional properties like hiding icons depending on a state etc.
To answer the question, you do not need to add self.backgroundView because it is already there. What you do need to do is set the frame in layoutSubviews as above but you need a UICollectionViewCell subclass in order to do that.
To use the custom cell you just need to include your new .h file and swap UICollectionViewCell for your new classname in the
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
method like:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CustomCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
cell.backgroundColor = [UIColor redColor];
return cell;
}
Do it like below, as you can not modify only height in frame you have to define new frame using CGRectMake function, after this you will also require to change the cell height also otherwise your view will be displayed in that much portion only.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
// .. set value for CGFloat backgroundHeight
[cell addSubview:cell.backgroundView];
CGRect frame = CGRectMake(cell.backgroundView.frame.origin.x, cell.backgroundView.frame.origin.y, cell.backgroundView.frame.size.width, backgroundHeight);
cell.backgroundView.frame = frame;
}
I need to have just simple UICollectionViewCell style with cells on top of eachoher. Like tableview. But I need Dynamic height dependent of the content, size the content is comments it can vary.
I got
viewDidLoad:
[self.commentsCollectionView registerClass:[GWCommentsCollectionViewCell class] forCellWithReuseIdentifier:#"commentCell"];
in .h I got:
and I #import my custom UICollectionViewCell that sets all constraints with programmatic autolayout.
I instantiate the UICollectionView with:
UICollectionViewFlowLayout *collViewLayout = [[UICollectionViewFlowLayout alloc]init];
self.commentsCollectionView = [[UICollectionView alloc]initWithFrame:CGRectZero collectionViewLayout:collViewLayout];
I use autolatyout to get the UICollectionView be where I want (thats why CGRectZero).
And finally I was hoping to do this:
-(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath{
GWCommentsCollectionViewCell *cell = (GWCommentsCollectionViewCell*)[collectionView cellForItemAtIndexPath:indexPath];
return cell.singleCommentContainerview.bounds.size;
}
singleCommentContainerview is a direct subview of the contentView and withing the singleCommentContainerview I have UILabels, UIImageViews etc, all set witih autolayoutcode.
But I just get cgsize value of (0,0)
How can I fix this to get the proper size I need for each cell?
From what I have read UICollectionView needs the sizes worked out before laying out the cell. So the above method of yours that cell hasn't yet been drawn so it has no size. Also it could be an issue or combined with the issue that the cell is cached/pooled with the same identifier #"commentCell", I tag unique cells with a new identifier and class normally.
My thoughts are to catch the cell before it is drawn, push the size into a dictionary for use later, using:
- (void)collectionView:(UICollectionView *)collectionView
willDisplayCell:(UICollectionViewCell *)cell
forItemAtIndexPath:(NSIndexPath *)indexPath{
GWCommentsCollectionViewCell *cell = (GWCommentsCollectionViewCell*)[collectionView cellForItemAtIndexPath:indexPath];
// Need to add it to the view maybe in order for it the autolayout to happen
[offScreenView addSubView:cell];
[cell setNeedsLayout];
CGSize *cellSize=cell.singleCommentContainerview.bounds.size
NSString *key=[NSString stringWithFormat:#"%li,%li",indexPath.section,indexPath.row];
// cellAtIndexPath is a NSMutableDictionary initialised and allocated elsewhere
[cellAtIndexPath setObject:[NSValue valueWithCGSize:cellSize] forKey:key];
}
Then when you need it use that dictionary based off the key to get the size.
Its not a really super pretty way as its dependent on the views being drawn, autolayout doing its thing before you get the size. And if you are loading images even more it could throw up issues.
Maybe a better way would be to preprogram the sizes. If you have data on the images sizes that may help. Check this article for a really good tutorial (yah programatically no IB):
https://bradbambara.wordpress.com/2014/05/24/getting-started-with-custom-uicollectionview-layouts/
Add
class func size(data: WhateverYourData) -> CGSize { /* calculate size here and retrun it */}
to your custom cell and instead of doing
return cell.singleCommentContainerview.bounds.size
it should be
return GWCommentsCollectionViewCell.size(data)
I have a collection view, and I'm going for the effect where when a cell is tapped on, it grows to take up the entire screen. To accomplish this, I'm essentially just calling performBatchUpdates inside didSelectItemAtIndexPath, and the sizeForItemAtIndexPath knows to return a larger size for a selected cell. This all works pretty well, the cell grows and shrinks as desired.
The problem is inside of the cell. The collection view cell is made up of a moderately complex view hierarchy managed by constraints. I want the sub views of the cell to grow and shrink with the animating cell. Unfortunately, my subviews are snapping immediately to their new position as the cell slowly animates to it's new size. How can I ensure the content of the cell animates with the cell size?
Here are the two relevant methods from the collection view controller:
- (CGSize) collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
if ([collectionView.indexPathsForSelectedItems containsObject:indexPath])
return CGSizeMake(collectionView.bounds.size.width - 20, collectionView.bounds.size.height - (20 + [self.topLayoutGuide length]));
else
return CGSizeMake(260, 100);
}
- (void) collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
[collectionView performBatchUpdates:nil completion:nil];
}
I had exactly the same problem. After many tries, the following code did the trick for me. Calling layoutIfNeed directly after the batch update with a duration of 3.0 (+- the batchupdate duration) animated the constraints.
[self performBatchUpdates:nil completion:nil];
// IndexPath of cell to be expanded
UICollectionViewCell *cell = [self cellForItemAtIndexPath:indexPath];
[UIView animateWithDuration:0.3 animations:^{
[cell.contentView layoutIfNeeded];
}];
I would like to expand on Antoine's answer. Calling performBatchUpdates:completion: inside the animation block will actually set the cell resize duration and match the content resize to it.
[UIView animateWithDuration:0.3 animations:^{
[self performBatchUpdates:^{
// set flags needed for new cell size calculation
} completion:nil];
[collectionView layoutIfNeeded];
}];
This way you can also play with the animation timing or even use spring animations.
I have the same problem. Add these to your subclass of CollectionViewCell. It works for me.
// ProblematicCollectionViewCell.swift
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
layoutIfNeeded()
}
Here is the origanl link : https://codedump.io/share/LXZwAOOMxafU/1/uicollectionviewcell---contents-do-not-animate-alongside-cell39s-contentview the answer is in the comments.
We are using a UICollectionView to display cell that cover the full screen (minus the status and nav bar). The cell size is set from self.collectionView.bounds.size:
- (void)viewWillAppear:(BOOL)animated
{
//
// value isn't correct with the top bars until here
//
CGSize tmpSize = self.collectionView.bounds.size;
_currentCellSize = CGSizeMake( (tmpSize.width), (tmpSize.height));
}
- (CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout*)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return _currentCellSize;
}
This sets the correct sizing for each device. Each cell is defined to have no insets, and the layout has no header or footer. However, when we rotate from portrait to landscape we get the following "complaint":
the behavior of the UICollectionViewFlowLayout is not defined because:
the item height must be less that the height of the UICollectionView minus the section insets top and bottom values.
Now I understand this error, however we reset the size of the cell and use the flow layouts built in rotation transition:
-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
//self.collectionView.bounds are still the last size...not the new size here
}
- (void)didRotateFromInterfaceOrientation: UIInterfaceOrientation)fromInterfaceOrientation
{
CGSize tmpSize = self.collectionView.bounds.size;
_currentCellSize = CGSizeMake( (tmpSize.width), (tmpSize.height));
[self.collectionView performBatchUpdates:nil completion:nil];//this will force the redraw/size of the cells.
}
The cells render correctly in landscape.
It seems as though the Flow Layout sees the old cell size (which causes the complaint since it will be too tall), but does read/render the new cell size set in didRotateFromInterfaceOrientation.
Is there a way to get rid of the complaint?
We've tried finding another hook during a device rotate transition that has access to the correct target screen size (vs the current screen size) with no luck. Debug output shows the complaint happens after willRotateToInterfaceOrientation but before didRotateFromInterfaceOrientation.
We've also verified the obvious; if we set up the cell height to be a fixed size less than the landscape screen height, the complaint doesn't occur. Also, the complaint does not occur when rotating from landscape back to portrait.
Everything runs fine, and renders correctly. However this complaint worries us. Anyone else have any ideas or solutions?
I was getting the same warning. Unsatisfied with the "reloadData" approach, I found that calling [self.collectionView.collectionViewFlowLayout invalidateLayout] before setting the frame of the collection view silenced the warning and yielded the expected results.
Not to throw another shrimp on this loaded, yet unaccepted, barbie.
- (CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
[collectionView setContentInset:UIEdgeInsetsMake(0, 0, 0, 0)];
return CGSizeMake(100, collectionView.frame.size.height);
}
Setting the content insets just before returning the cell size did the trick for me.
Note:
I am using a container view in a storyboard to load the collection view within a UIViewController. I tried setting this on the flowLayout object in the storyboard. The collection view in the storyboard. And overriding one of the UICollectionViewDelegateFlowLayout; though I do not remember which one. I'm also not sure if this will work for a vertical layout.
In
[UIViewController willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration]
I called the, [UICollectionViewLayout invalidateLayout] and seems to work good.
I solved it.
You should just let the height of flowLayout less than collcetionView.frame.size.height.
[flowLayout setItemSize:CGSizeMake(width, height)];
collectionView.frame = CGRectMake(12, 380, 290, 80);
A lot of the solutions suggest adding invalidateLayout to willAnimateRotationToInterfaceOrientation - but this is deprecated since iOS 8.
For iOS 8 and higher, use:
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[self.collectionView.collectionViewLayout invalidateLayout];
}
Thanks to #user7097242's comment here is a swift4 version:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
self.collectionView.collectionViewLayout.invalidateLayout()
}
I encountered this same issue. If the collection view was displayed when in portrait orientation, the cells would disappear when rotated to landscape. I did a combination of the other answers here to fix this.
I set my view (the one that contains the UICollectionView) up to receive the UIDeviceOrientationDidChangeNotification notification. In the method that responds to that notification, after the UICollectionView frame was adjusted, I did the following:
- (void)orientationChanged:(NSNotification *)notification
{
CGSize size = (CGSize){self.frame.size.width - 2*kContentMargin, self.frame.size.height - 2*kContentMargin};
[self.collectionView.collectionViewLayout setItemSize:size];
[self.collectionView.collectionViewLayout invalidateLayout];
[self.collectionView reloadData];
}
Note that the frame of the UICollectionView is being set automatically upon rotation because its resizingMask is set to this upon initialization:
self.collectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
Just encountered and fixed the same problem. Since my solution is more along the lines you were asking for and doesn't match any existing answer, I've posted it here.
#define NUMBER_OF_CELLS_PER_ROW 1
- (UICollectionViewFlowLayout *)flowLayout {
return (UICollectionViewFlowLayout *)self.collectionViewLayout;
}
- (CGSize)itemSizeInCurrentOrientation {
CGFloat windowWidth = self.collectionView.window.bounds.size.width;
CGFloat width = (windowWidth - (self.flowLayout.minimumInteritemSpacing * (NUMBER_OF_CELLS_PER_ROW - 1)) - self.flowLayout.sectionInset.left - self.flowLayout.sectionInset.right)/NUMBER_OF_CELLS_PER_ROW;
CGFloat height = 80.0f;
return CGSizeMake(width, height);
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
[super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
[self.flowLayout invalidateLayout];
self.flowLayout.itemSize = [self itemSizeInCurrentOrientation];
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
// Now that the rotation is complete, load the cells.
[self.collectionView reloadData];
}
This solved it for me:
-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
[self.collectionView.collectionViewLayout invalidateLayout];
}
And I am using this delegate method for setting the size:
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
And I can from there use this with the correct frame after rotation:
self.collectionView.frame.size
My fix was as simple as unchecking 'Adjust Scroll View Insets' for the view controller in IB, since I needed my navigation bar to be translucent.
This works for me: (and hope it also works for you!)
- (void)viewWillLayoutSubviews
{
self.flowLayout.itemSize = CGSizeMake(self.collectionView.bounds.size.width/4,self.collectionView.bounds.size.height);
[self.flowLayout invalidateLayout];
}
- (void)viewDidLayoutSubviews
{
self.flowLayout.itemSize = CGSizeMake(self.collectionView.bounds.size.width/4,self.collectionView.bounds.size.height);
}
I faced the same problem.
here is how i solved it. hope it helps
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:( NSTimeInterval)duration
{
[super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
[self.collectionView reloadData];
}
I ran into the same problem when resizing the frame of a UICollectionView. If I used the delegate method on FlowLayout to return the size of the cell (which would be updated based on the size of the containing UICollectionView), I would get the error message when I resized (smaller) the frame of the UICollectionView, since it didn't seem to ask the delegate for updated size information before complaining. It would eventually ask the delegate method for size info when redrawing, but it would still issue the warning at the time I assigned a new frame method. To get rid of the warning, I explicitly set the itemSize property of the UICollectionViewFlowLayout object before I set the frame to a new smaller value. My situation is simple enough that I think I can get away with doing the itemSize calculation at this point (since all my items in the collection view are the same size), instead of depending on the delegate method. Just setting the itemSize property while leaving the delegate method implemented did not solve the problem, as I think it ignored the value of itemSize if it detected that the delegate method was implemented (if it knows it is there, why doesn't it call it?!). Hopefully this helps - perhaps you can also explicitly set the itemSize before rotation.
Just to suppress warning and (probably, not sure) improve performance you could before returning size in
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
check if view controller is in process of rotation and if it is return some relatively small size like CGSizeMake(0.1, 0.1)
You can subclass UICollectionView and override setBounds:. There before calling [super setBounds:] the item size can be adjusted to the new bounds.
You should check whether the size of the bounds has changed, because setBounds: is invoked also while scrolling.
Try this...
UICollectionViewFlowLayout *layout = (id) self.collectionView.collectionViewLayout;
layout.itemSize = CGSizeMake(0.1, 0.1);
It's works for me.
Following code fixed it for me:
-(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return self.collectionView.frame.size;
}
I've used an UICollectionViewFlowLayoutInvalidationContext, in which I calculate the new offset such that it maintains the same content offset. My own function collectionViewSizeForOrientation: returns the proper size. Its not perfect, but at least it's not sketchy:
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
CGSize fromCollectionViewSize = [self collectionViewSizeForOrientation:[self interfaceOrientation]];
CGSize toCollectionViewSize = [self collectionViewSizeForOrientation:toInterfaceOrientation];
CGFloat currentPage = [_collectionView contentOffset].x / [_collectionView bounds].size.width;
NSInteger itemCount = [_collectionView numberOfItemsInSection:0];
UICollectionViewFlowLayoutInvalidationContext *invalidationContext = [[UICollectionViewFlowLayoutInvalidationContext alloc] init];
[invalidationContext setContentSizeAdjustment:CGSizeMake(toCollectionViewSize.width * itemCount - fromCollectionViewSize.width * itemCount, toCollectionViewSize.height - fromCollectionViewSize.height)];
[invalidationContext setContentOffsetAdjustment:CGPointMake(currentPage * toCollectionViewSize.width - [_collectionView contentOffset].x, 0)];
[[_collectionView collectionViewLayout] invalidateLayoutWithContext:invalidationContext];
[super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
}
collectionViewSizeForOrientation: in my case is the following, assuming that insets and item spacing are 0:
- (CGSize)collectionViewSizeForOrientation:(UIInterfaceOrientation)orientation
{
CGSize screenSize = [[UIScreen mainScreen] bounds].size;
CGFloat width = UIInterfaceOrientationIsLandscape(orientation) ? MAX(screenSize.width, screenSize.height) : MIN(screenSize.width, screenSize.height);
CGFloat height = UIInterfaceOrientationIsLandscape(orientation) ? MIN(screenSize.width, screenSize.height) : MAX(screenSize.width, screenSize.height);
return CGSizeMake(width, height);
}
Trying to find a solution to silence these warnings on iOS 7 was proving difficult for me. I ended up resorting to subclassing my UICollectionView and added the following code.
- (void)setFrame:(CGRect)frame
{
if (!iOS8 && (frame.size.width != self.frame.size.width))
{
[self.collectionViewLayout invalidateLayout];
}
[super setFrame:frame];
}
Some might want to do a whole size check with CGSizeEqualToSize().
try at the end of didRotateFromInterfaceOrientation:
[self.collectionView setNeedsLayout]
if it does not work try to move all this stuff from didRotateFromInterfaceOrientation to willRotateToInterfaceOrientation
Note: In my case, I discovered that we were setting the preferredContentSize property of the UIViewController in question. If you find this to be your case, you might have to deal with the following method of the UICollectionViewDelegateFlowLayout protocol:
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
CGSize size = collectionViewLayout.collectionView.bounds.size;
...
return size;
}
I know this is an old question, but I just got the same problem and spent an hour to solve it. My problem was that, it seems the UICollectionView's frame size is always wrong (the height doesn't match the container) while I set the frame size right before the UICollectionView Flow layout delegate is called. So, I set the UICollectionView frame size again on the method :
-(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
And that did the trick. The height is now showing correctly and the warning is gone.
SUMMARY
Given that we don't always know what the frame of a cell or its content view is going to be (due to editing, rotation, accessory views etc.), what is the best way to calculate the height in tableView:heightForRowAtIndexPath: when the cell contains a variable height text field or label?
One of my UITableViewController's contains the following presentation:
UITableViewCell with UITextView.
UITextView should be the same width and height as UITableViewCell.
I created the UITableViewCell subclass, and then and initialized it with UITextView (UITextView is a private field of my UITableViewController)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *CellIdentifier = #"TextViewCell";
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[BTExpandableTextViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier textView:_notesTextView] autorelease];
}
return cell;
}
I implemented the following method in my UITableViewCell subclass:
- (void)layoutSubviews{
[super layoutSubviews];
CGFloat height = [textView.text sizeWithFont:textView.font constrainedToSize:CGSizeMake(textView.frame.size.width, MAXFLOAT)].height + textView.font.lineHeight;
textView.frame = CGRectMake(0, 0, self.contentView.frame.size.width, (height < textView.font.lineHeight * 4) ? textView.font.lineHeight * 4 : height);
[self.contentView addSubview:textView];
}
and of course i implemented the following UITableViewDataSource method (look! I am using self.view.frame.size.width (but really i need UITableViewCell contentView frame width):
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath{
CGFloat height = [_notesTextView.text sizeWithFont:_notesTextView.font
constrainedToSize:CGSizeMake(self.view.frame.size.width, MAXFLOAT)].height;
CGFloat groupedCellCap = 20.0;
height += groupedCellCap;
if(height < [BTExpandableTextViewCell minimumTextViewHeightWithFont:_notesTextView.font]){
height = [BTExpandableTextViewCell minimumTextViewHeightWithFont:_notesTextView.font];
}
return height;
}
also I implemented the following method (thats not so important but ill post it anyway, just to explain that cell's height is dynamical, it will shrink or expand after changing text in UITextView)
- (void)textViewDidChange:(UITextView *)textView{
CGFloat height = [_notesTextView.text sizeWithFont:_notesTextView.font
constrainedToSize:CGSizeMake(_notesTextView.frame.size.width, MAXFLOAT)].height;
if(height > _notesTextView.frame.size.height){
[self.tableView beginUpdates];
[self.tableView endUpdates];
}
}
And now, my question is:
After loading view, UITableViewController is calling methods in the following order: (ill remove some, like titleForHeaderInSection and etc for simplification)
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath{
and only then
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
Look! I should return the correct UITableViewCell height before cellForRowAtIndexPath!
That means: I don't know UITableViewCell contentView frame. And i can't get it programmatically.
This width can be one of:
iPhone plain table, portrait orientation
iPhone plain table, landscape orientation
iPhone grouped table, portrait orientation
iPhone grouped table, landscape orientation
and the same for the iPad ( another 4 values )
And don't forget that contentView frame can be smaller because of UITableViewCell accessoryType, or because of UITableView editing state. (for example if we have UITableViewCell with multiline UILabel of any height in any editing state and with any accessoryView)
So this problem is fundamental: I just can't get cell contentView frame width for constraining, because I should return this height before cell layouts contentView. (And this is pretty logical, by the way) But this contentView frame really matters.
Of course sometimes I can know this width exactly and "hardcode" it
(for example: UITableViewCellAccessoryDisclosureIndicator has 20 px width, and tableView cannot be in editing state, then I can write self.view.frame.size.width - 20 and the task is done)!
Or sometimes contentView is equal to UITableViewController's view frame!
Sometimes I'm using self.view.frame.width in -tableView:heightForRowAtIndexPath: method.. (like now, and it works pretty well, but not perfectly because of grouped UITableView, should subtract some constant values, and they are different for 2 devices * 2 orientations)
Sometimes I have some #defined constants in UITableViewCell (if I know width exactly)...
Sometimes I'm using some dummy pre-allocated UITableViewCell (what is just stupid, but sometimes is pretty elegant and easy for use)...
But I don't like anything of that.
What's the best decision?
Maybe i should create some helper class, that will be initialized with such parameters:
accessory views, device orientation, device type, table view editing state, table view style (plain, grouped), controller view frame, and some other, that will include some constants (like grouped tableView offset, etc) and use it to find the expected UITableViewCell contentView width? ;)
Thanks
Table view uses the tableView:heightForRowAtIndexPath: method to determine its contentSize before creating any UITableViewCellcells. If you stop and think about it, this makes sense, as the very first thing you would do with a UIScrollView is set its contentSize. I have run into a similar problem before, and what I've found is that it is best to have a helper function that can take the content going into the UITableViewCell and predict the height of that UITableViewCell. So I think you will want to create some sort of data structure that stores the text in each UITableViewCell, an NSDictionary with NSIndexPaths as keys and the text as values would do nicely. That way, you can find the height of the text needed without referencing the UITableViewCell.
Although you can calculate heights for labels contained in table view cells, truly dynamically, in '- layoutSubviews' of a UITableViewCell subclass, there's no similar way of doing this (that I know of) for cell heights in '- tableView:heightForRowAtIndexPath:' of a table view delegate.
Consider this:
- (void)layoutSubviews
{
[super layoutSubviews];
CGSize size = [self.textLabel.text sizeWithFont:self.textLabel.font
constrainedToSize:CGSizeMake(self.textLabel.$width, CGFLOAT_MAX)
lineBreakMode:self.textLabel.lineBreakMode];
self.textLabel.$height = size.height;
}
Unfortunately though, by the time '- tableView:heightForRowAtIndexPath:' is called, that is too early, because cell.textLabel.frame is yet set to CGRectZero = {{0, 0}, {0, 0}}.
AFAIK you won't be able to do this neither with content view's frame, nor summing up individual labels' frames...
The only way I can think of is to come up with a convenience class, methods, constants, or such that will try to cover up all possible width in any device orientation, on any device:
#interface UITableView (Additions)
#property (nonatomic, readonly) CGFloat padding;
#end
#implementation UITableView (Additions)
- (CGFloat)padding
{
if (self.formStyle == PTFormViewStylePlain) {
return 0;
}
if (self.$width < 20.0) {
return self.$width - 10.0;
}
if (self.$width < 400.0 || [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
return 10.0;
}
return MAX(31.0, MIN(45.0, self.$width * 0.06));
}
#end
Also note that, recently we also have new iPhone 5's 4-inch width (568 instead of 480) in landscape orientation.
This whole thing is pretty disturbing, I know... Cheers.