I have a (horizontal) collection-view, and I change its size via auto layout constraints. However, when the collection-view's size changes, the cell remains the same static size.
To demonstrate, here's the collection-view in the view debugger (I've labeled both the collectionview and one of its cells):
As you can see, the cell remains the original size and is now actually taller than its encasing collection-view.
So how can I constrain the cell to always fit within its collection-view?
If you want to resize your view you can use the following to reset the item size on rotation, this would go inside your view controller subclass.
This should be fairly efficent as it is only called when the size of the visible view controller changes.
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator;
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[coordinator animateAlongsideTransition:({
^(id<UIViewControllerTransitionCoordinatorContext> context) {
UICollectionViewFlowLayout *layout = (id)self.collectionView.collectionViewLayout; {
CGFloat height = CGRectGetHeight(self.collectionView.bounds);
layout.itemSize = (CGSize) {
.width = height,
.height = height
};
}
[layout invalidateLayout];
};
}) completion:nil];
}
You should also check out these answers for some more solutions.
I solved this through setting the item's height and width equal to the collection-view's height. I do this in sizeForItemAtIndexPath:
-(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return CGSizeMake(collectionView.frame.size.height, collectionView.frame.size.height);
}
Because in my case the cell's need to be perfect squares, setting the cell's height and width to the height of the collection-view works perfectly.
Not that your class must implement the UICollectionViewDelegateFlowLayout protocol for this method to be exposed.
I have made my collection view cells to be calculated programmatically and I'm using sizeForItemAtIndexPath method to set the cell's sizes.
But When the screen rotates ,I need to recalculate the sizes and set them for cells. So,How can I call that function again after screen rotates?
By the way, I know didRotateFromInterfaceOrientation method and the only problem I got is that I don't know how should I call set the cell's sizes again.
Thanks
Another way using newer UIViewController methods
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context)
{
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context)
{
[self.collectionView.collectionViewLayout invalidateLayout];
}];
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
}
You can reload your collection view in this method: This method is called when device changes its orientation. Reload collectionview will call sizeForItemAtIndexPath.
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
[collectionView reloadData]; // change collectionView with your collection view
}
Calculating the dynamics of cell can be replaced with auto calculation.
UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *) self.collectionView.collectionViewLayout;
flowLayout.estimatedItemSize = CGSizeMake(estimatedWidth, estimatedHeight)];
Try this
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
[collectionView reloadInputViews];
}
This is the view of the app when it is in portrait mode.
When it is rotated to landscape mode it looks like this
The view debugger shows that the UIWindow is not rotating as shown here
The UICollectionViewController is created via StoryBoard. I've tried subclassing UICollectionViewFlowLayout that implements shouldInvalidateLayoutForBoundsChange, but it does not fix my issue.
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
CGRect oldBounds = self.collectionView.bounds;
if (CGRectGetWidth(newBounds) != CGRectGetWidth(oldBounds)) {
return YES;
}
return NO;
}
Please provide ideas of what to check next or requests for additional code to debug.
Edit - As suggested by MirekE, I attempted to add constraints to the CollectionView but was unable. All of the options for Editor->Pin are unavailable for the CollectionView.
Edit, response to Andrea -
I'm targeting iOS8.3. My understanding is that the main method called at rotation is viewWillTransitionToSize:withTransitionCoordinator:, which is from the UIContentContainer protocol. I've added the following to my CollectionViewController, but same problem persists
-(void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator {
[self.collectionView.collectionViewLayout invalidateLayout];
[self.collectionView reloadData];
}
I don't know which iOS version you ara targeting, but let me suppose that you know the rotation process and methods called in the view controller while it's happening.
In one of those methods you just need to call:
[self.collectionView.collectionViewLayout invalidateLayout];
and maybe depending on your layout -reloadData
No need to subclass.
EDIT
I use this method, I guess that is not working because you should relayout the collection after it has resized:
- (void) willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super willTransitionToTraitCollection:newCollection withTransitionCoordinator:coordinator];
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
[self.collectionView.collectionViewLayout invalidateLayout];
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
[self.collectionView reloadData];
}];
}
What I'm doing attach the layout invalidation process to the animation process.
If you have custom implementation of UICollectionViewLayout, try to override methods:
override public func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
let bounds = self.collectionView!.bounds;
return ((CGRectGetWidth(newBounds) != CGRectGetWidth(bounds) ||
(CGRectGetHeight(newBounds) != CGRectGetHeight(bounds))));
}
override public func invalidateLayout() {
cache.removeAll() // remove layout attributes ans settings if they exist
super.invalidateLayout()
}
Cheers !
It looks like you just drag the collection view on the canvas, but did not add any constraints. So when you rotate the device, the size does not change.
In the storyboard select the collection view, then click on the Pin icon at the bottom of Xcode and add constraints for the top, left, bottom and right margins. After you do that, the collection view should resize on rotation.
The problem was that the collectionView outlet was not properly set. After setting the collectionView, all works properly.
collectionView outlet not set:
collectionView outlet set:
In iOS 7, given a UICollectionView, how do you start it at the bottom? Think about the iOS Messages app, where when the view becomes visible it always starts at the bottom (most recent message).
#awolf
Your solution is good!
But do not work well with autolayout.
You should call [self.view layoutIfNeeded] first!
Full solution is:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// ---- autolayout ----
[self.view layoutIfNeeded];
CGSize contentSize = [self.collectionView.collectionViewLayout collectionViewContentSize];
if (contentSize.height > self.collectionView.bounds.size.height) {
CGPoint targetContentOffset = CGPointMake(0.0f, contentSize.height - self.collectionView.bounds.size.height);
[self.collectionView setContentOffset:targetContentOffset];
}
}
The problem is that if you try to set the contentOffset of your collection view in viewWillAppear, the collection view hasn't rendered its items yet. Therefore self.collectionView.contentSize is still {0,0}. The solution is to ask the collection view's layout for the content size.
Additionally, you'll want to make sure that you only set the contentOffset when the contentSize is taller than the bounds of your collection view.
A full solution looks like:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
CGSize contentSize = [self.collectionView.collectionViewLayout collectionViewContentSize];
if (contentSize.height > self.collectionView.bounds.size.height) {
CGPoint targetContentOffset = CGPointMake(0.0f, contentSize.height - self.collectionView.bounds.size.height);
[self.collectionView setContentOffset:targetContentOffset];
}
}
This works for me and i think it is a modern way.
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.collectionView!.scrollToItemAtIndexPath(indexForTheLast, atScrollPosition: UICollectionViewScrollPosition.Bottom, animated: false)
}
yourCollectionView.contentOffset = CGPointMake(0, yourCollectionView.contentSize.height - yourCollectionView.bounds.size.height);
But remember to do this only when your contentSize.height > bounds.size.height.
Assuming that you know how many items are in your collection view you can use
scrollToItemAtIndexPath:atScrollPosition:animated:
Apple Docs
it works perfectly for me (autolayout)
Calculate ScrollView's Content Size using collectionViewFlowLayout and cellSize
collectionView.contentSize = calculatedContentSize
collectionView.scrollToItemAtIndexPath(whichYouWantToScrollIndexPath, atScrollPosition: ...)
added in scrollToItemAtIndexPath:atScrollPosition:animated: in viewWillLayoutSubviews so that the collectionView will be loaded instantly at the bottom
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.