70

2015-08-18 16:07:51.523 Example[16070:269647] the behavior of the UICollectionViewFlowLayout is not defined because: 2015-08-18 16:07:51.523
Example[16070:269647] the item width must be less than the width of the UICollectionView minus the section insets left and right values, minus the content insets left and right values.
2015-08-18 16:07:51.524 Example[16070:269647] The relevant UICollectionViewFlowLayout instance is , and it is attached to ; animations = { position=; bounds.origin=; bounds.size=; }; layer = ; contentOffset: {0, 0}; contentSize: {1024, 770}> collection view layout: . 2015-08-18 16:07:51.524 Example[16070:269647] Make a symbolic breakpoint at UICollectionViewFlowLayoutBreakForInvalidSizes to catch this in the debugger.

This is what I get, what I do is

func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
        return CGSizeMake(self.collectionView!.frame.size.width - 20, 66)
    }

when I rotate from landscape to portrait, the console shows this error message only in iOS 9, does anyone know what this happens and if there is a fix for this?

Screenshot

Jayprakash Dubey
  • 32,447
  • 16
  • 161
  • 169
Shabarinath Pabba
  • 1,169
  • 2
  • 10
  • 21

21 Answers21

50

This happens when your collection view resizes to something less wide (go from landscape to portrait mode, for example), and the cell becomes too large to fit.

Why is the cell becoming too large, as the collection view flow layout should be called and return a suitable size ?

collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath)

Update to include Swift 4

@objc override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{  ...  }

This is because this function is not called, or at least not straight away.

What happens is that your collection view flow layout subclass does not override the shouldInvalidateLayoutForBoundsChange function, which returns false by default.

When this method returns false, the collection view first tries to go with the current cell size, detects a problem (which logs the warning) and then calls the flow layout to resize the cell.

This means 2 things :

1 - The warning in itself is not harmful

2 - You can get rid of it by simply overriding the shouldInvalidateLayoutForBoundsChange function to return true. In that case, the flow layout will always be called when the collection view bounds change.

Milad Faridnia
  • 8,038
  • 13
  • 63
  • 69
AirXygène
  • 1,909
  • 11
  • 28
  • I have tried all possible suggestions on the internet and this one is the right answer!!! Thank you very much for your detailed explanation and simple solution! If I could I would upvote this answer 10 times. – ElectroBuddha Jun 21 '17 at 16:40
  • 1
    Thank you, this worked for me. UPDATE for swift 4: change "sizeForItemAtIndexPath" to "sizeForItem" or it will NOT work, and NOT show any errors. – David Sanford Dec 13 '17 at 05:44
  • 9
    From WWDC'18 *A Tour of UICollectionView*, it's mentioned that this method `shouldInvalidateLayoutForBoundsChange` is called even when **scrolling** the collection view. So the speaker particular cautions you to be careful when overriding this method, since it's called pretty often. – HuaTham Jun 26 '18 at 13:09
  • Here's an alternative solution of this issue which worked for me: https://stackoverflow.com/a/53117524/5242940 – Legonaftik Nov 02 '18 at 11:58
  • This did not solve it for me. I ended up just recreating the `UICollectionView` when the device rotates. It's not like it happens all that often. – Tjalsma May 27 '19 at 22:39
  • What is the `shouldInvalidateLayoutForBoundsChange` function. I can't find it for swift – Sam Dec 17 '19 at 03:51
  • @Sam shouldInvalidateLayout(forBoundsChange:) – AirXygène Dec 17 '19 at 06:12
  • Weird, that didn't seem to help me, It resizes at first but then it gets big again. I have a bountied question on similar stuff if you interested https://stackoverflow.com/questions/58617380/setting-and-changing-width-height-of-uicollectionview-cells-with-pinch-gestures – Sam Dec 17 '19 at 06:38
  • 1
    This certainly sounds correct, but I implemented this override to return true, set the custom layout class, and ran it in iOS 12.2 simulator, saw it call my override, and still spewed the warning repeatedly. – androidguy Dec 03 '20 at 03:37
  • Is it possible to call `shouldInvalidateLayoutForBoundsChange` without subclassing the layout? – Gizmodo Mar 25 '21 at 16:22
33

select collectionview in storyboard go to size inspector change estimate size to none

Satish Kumar
  • 339
  • 3
  • 2
18

I've also seen this occur when we set the estimatedItemSize to automaticSize and the computed size of the cells is less than 50x50 (Note: This answer was valid at the time of iOS 12. Maybe later versions have it fixed).

i.e. if you are declaring support for self-sizing cells by setting the estimated item size to the automaticSize constant:

flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

and your cells are actually smaller than 50x50 points, then UIKit prints out these warnings. FWIW, the cells are eventually sized appropriately and the warnings seem to be harmless.

One fix workaround is to replace the constant with a 1x1 estimated item size:

flowLayout.estimatedItemSize = CGSize(width: 1, height: 1)

This does not impact the self-sizing support, because as the documentation for estimatedItemSize mentions:

Setting it to any other value causes the collection view to query each cell for its actual size using the cell’s preferredLayoutAttributesFitting(_:) method.

However, it might/might-not have performance implications.

Manav
  • 9,066
  • 6
  • 35
  • 50
  • 6
    If the covid-19 wouldn't be with us these days, I swear I would travel wherever you are at the moment and hug you so much lol you saved my day man This drove me crazy. Looked at a lot of tutorials for setting up self-sizing cells and even in my own project had them setup correctly, but... they were all over 50pt. Thank you very much! – mdonati Apr 08 '20 at 19:44
  • This is very obvious now that you've said it! Thanks for providing this answer – mattsven Sep 22 '20 at 11:33
  • Thank you! This variable is clearly not an `automaticSize`, it's a `standardEstimatedSize`. Why the variable is named that way is beyond me. – Nathan F. Jan 12 '21 at 20:53
17

XCode 11

For me, the reason was that the CollectionView's Estimate option size was set to Automatic. It seems it is the default behavior in XCode 11.

Hence for me which I loaded the images in the cells, because of the size of the images and the automatic option (which wants to adapt the size of the cell with the size of the image) the cell's width became greater than the CollectionView's width.

By setting this option to None the problem solved.

where the automatic option is located

Navid
  • 595
  • 5
  • 18
9

I've solve this problem by using safeAreaLayoutGuide.

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

    return CGSize(width: (view.safeAreaLayoutGuide.layoutFrame.width), height: 80);
}

and you also have to override this function to support portrait and landscape mode correctly.

 override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    collectionView?.collectionViewLayout.invalidateLayout();
}
Luis Santiago
  • 106
  • 2
  • 7
7

I am using a subclass of UICollectionViewFlowLayout and using its itemSize property to specify the cell size (instead of the collectionView:sizeForItemAtIndexPath: delegate method). Every time I rotate the screen to a shorter width one (e.g. landscape -> portrait) I get this huge warning in question.

I was able to fix it by doing 2 steps.

Step 1: In UICollectionViewFlowLayout subclass's prepareLayout() function, move super.prepareLayout() to after where self.itemSize is set. I think this makes the super class to use the correct itemSize value.

import UIKit

extension UICollectionViewFlowLayout {

  var collectionViewWidthWithoutInsets: CGFloat {
    get {
      guard let collectionView = self.collectionView else { return 0 }
      let collectionViewSize = collectionView.bounds.size
      let widthWithoutInsets = collectionViewSize.width
        - self.sectionInset.left - self.sectionInset.right
        - collectionView.contentInset.left - collectionView.contentInset.right
      return widthWithoutInsets
    }
  }

}

class StickyHeaderCollectionViewLayout: UICollectionViewFlowLayout {

  // MARK: - Variables
  let cellAspectRatio: CGFloat = 3/1

  // MARK: - Layout
  override func prepareLayout() {
    self.scrollDirection = .Vertical
    self.minimumInteritemSpacing = 1
    self.minimumLineSpacing = 1
    self.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: self.minimumLineSpacing, right: 0)
    let collectionViewWidth = self.collectionView?.bounds.size.width ?? 0
    self.headerReferenceSize = CGSize(width: collectionViewWidth, height: 40)

    // cell size
    let itemWidth = collectionViewWidthWithoutInsets
    self.itemSize = CGSize(width: itemWidth, height: itemWidth/cellAspectRatio)

    // Note: call super last if we set itemSize
    super.prepareLayout()
  }

  // ...

}

Note that the above change will somehow make the layout size change when screen rotates stops working. This is where step 2 comes in.

Step 2: Put this in the view controller that holds the collection view.

  override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
    collectionView.collectionViewLayout.invalidateLayout()
  }

Now the warning is gone :)


Some Notes:

  1. Make sure you are adding constraints to the collectionView and not using collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

  2. Make sure you are not calling invalidateLayout in viewWillTransitionToSize() because the width of an edge-to-edge cell in landscape is larger than the collection view’s frame width in portrait. See below references.

References

Mick
  • 26,684
  • 12
  • 104
  • 126
Hlung
  • 12,632
  • 6
  • 66
  • 87
  • 1
    ill look at this solution, the answer came a lot later than I went out from the project :( – Shabarinath Pabba Mar 18 '16 at 21:56
  • That works @Hlung, I hope you don't mind, I've added some notes as there are other requirements to prevent errors like these. – Mick Jul 16 '18 at 21:01
  • The key for me to fix warnings was to call [super prepareLayout] after the itemSize was set. Thank you @Hlung! – Jona Jan 30 '19 at 22:59
6

For Xcode 11, none of the above methods worked for me.

It turned out one need to set the Estimate Size to None in the collection view size panel.

see: https://forums.developer.apple.com/thread/123625

Yan
  • 137
  • 1
  • 8
5

Its happens because your collection view cell's width is bigger than collection view width after rotation.suppose that you have a 1024x768 screen and your collection view fills the screen.When your device is landscape,your cell's width will be self.collectionView.frame.size.width - 20 =1004 and its greater than your collection view's width in portrait = 768.The debugger says that "the item width must be less than the width of the UICollectionView minus the section insets left and right values, minus the content insets left and right values".

  • 1
    @Magoo You can fix it by changing collection view flow layout values for item size. The delegate must consider rotation. As the debugger says, the item width must be less than the width of the UICollectionView minus the section insets left and right values, minus the content insets left and right values. – mohammadrhemmati Oct 21 '17 at 14:57
4

This works for me:

Invalidate the layout on rotation:

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    collectionView.collectionViewLayout.invalidateLayout()
}

Have a separate function for calculating the available width:

Note: taking in to account both section inset and content inset.

// MARK: - Helpers

func calculateCellWidth(for collectionView: UICollectionView, section: Int) -> CGFloat {
    var width = collectionView.frame.width
    let contentInset = collectionView.contentInset
    width = width - contentInset.left - contentInset.right

    // Uncomment the following two lines if you're adjusting section insets
    // let sectionInset = self.collectionView(collectionView, layout: collectionView.collectionViewLayout, insetForSectionAt: section)
    // width = width - sectionInset.left - sectionInset.right

    // Uncomment if you are using the sectionInset property on flow layouts
    // let sectionInset = (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.sectionInset ?? UIEdgeInsets.zero
    // width = width - sectionInset.left - sectionInset.right

    return width
}

And then of course finally returning the item size:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CGSize(width: calculateCellWidth(for: collectionView, section: indexPath.section), height: 60)
}

I think it is important to note that this works because invalidating a layout wont trigger a recalculation of the cell size immediately, but during the next layout update cycle. This means that once the item size callback is eventually called, the collection view has the correct frame, thus allowing accurate sizing.

Voila!

josh-fuggle
  • 2,666
  • 2
  • 23
  • 24
  • Hi this line got error: let sectionInset = self.collectionView(collectionView, layout: collectionView.collectionViewLayout, insetForSectionAt: section). do u have sample project that make it work. thank – Lê Khánh Vinh Oct 08 '18 at 20:05
  • Hey @LêKhánhVinh, that's because you're not implementing that particular optional delegate method. You can just comment out those lines, I'll update my post too. – josh-fuggle Oct 09 '18 at 06:28
  • Hi anyway to auto adjust the height of cell base on content instead of return fix value 60? – Lê Khánh Vinh Oct 09 '18 at 06:44
  • @LêKhánhVinh yes there is a way to do it with collection views. You can research "self sizing cells" and UICollectionViewFlowLayoutAutomaticSize. – josh-fuggle Oct 09 '18 at 08:04
  • Only writing view will layout subview method helped me to solve the issue. +1 – mAc Nov 06 '18 at 07:51
4

Turn your collection view's estimate size 'None' value from automatic.

You can do this by the storyboard or programatically.

But you have to calculate it manually with custom layout class or 'UICollectionViewDelegateFlowLayout' protocol APIs

3

You can check in debugger if collectionView.contentOffset is changed to negative in my case it changes from (0,0) to (0,-40). You can solve this issue by using this method

if ([self respondsToSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)]) {
   self.automaticallyAdjustsScrollViewInsets = NO;
}
alternatiph
  • 334
  • 1
  • 4
  • 17
Anshul
  • 3,894
  • 3
  • 10
  • 11
3

After doing some experimenting, this seems to also be tied to how you layout your collectionView.

The tl;dr is: Use AutoLayout, not autoresizingMask.

So for the core of the problem the best solutions I've found to handle orientation change use the following code, which all makes sense:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)

    coordinator.animate(alongsideTransition: { (context) in
        self.collectionView.collectionViewLayout.invalidateLayout()
    }, completion: nil)
}

However, there are still situations where you can get the item size warning. For me it's if I am in landscape in one tab, switch to another tab, rotate to portrait, then return to the previous tab. I tried invalidating layout in willAppear, willLayout, all the usual suspects, but no luck. In fact, even if you call invalidateLayout before super.willAppear() you still get the warning.

And ultimately, this problem is tied to the size of the collectionView bounds updating before the delegate is asked for the itemSize.

So with that in mind I tried using AutoLayout instead of collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight], and with that, problem solved! (I use SnapKit to do AutoLayout so that I don't pull my hair out constantly). You also just need to invalidateLayout in viewWillTransition (without the coordinator.animate, so just as you have in your example), and also invalidateLayout at the bottom of viewWillAppear(:). That seems to cover all situations.

I dont know if you are using autoresizing or not - it would be interesting to know if my theory/solution works for everyone.

Loz
  • 1,922
  • 2
  • 17
  • 20
  • I managed to resolve by invalidating the layout in `viewWillLayoutSubviews` ``` override open func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() self.collectionView.collectionViewLayout.invalidateLayout() } ``` – Lev Landau Mar 26 '18 at 09:53
1

I had a similar issue.

In my case, I had a collection view and when you tapped on one of the cells, a popover with a UITextField opened, to edit the item. After that popover disappeared, the self.collectionView.contentInset.bottom was set to 55 (originally 0).

To fix my issue, after the popover view disappears, I’m manually setting contentInset to UIEdgeInsetsZero.

contextual prediction bar

The original issue seems to be related to the contextual prediction bar that shows up on top of the keyboard. When the keyboard is hidden, the bar disappears, but the contentInset.bottom value is not restored to the original value.

Since your issue seems to be related to the width and not to the height of the cell, check if any of the contentInset or layout.sectionInset values are the same as the one set by you.

Marius
  • 3,531
  • 3
  • 25
  • 30
1

I had the same issue. I fixed this by clearing my collection view of it's constraints, and resetting them in Storyboard.

Alien
  • 444
  • 2
  • 7
  • 26
1

I could solve it by putting super.prepare() at end of override func prepare() { }

Wasim
  • 571
  • 7
  • 10
0

I found that this worked quite well for UICollectionViewController and animated correctly:

- (void)viewWillTransitionToSize:(CGSize)size
       withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
    // Ensure the layout is within the allowed size before animating below, or 
    //  `UICollectionViewFlowLayoutBreakForInvalidSizes` breakpoint will trigger
    //  and the logging will occur.

    if ([self.collectionView.collectionViewLayout isKindOfClass:[YourLayoutSubclass class]]) {
        [(YourLayoutSubclass *)self.collectionView.collectionViewLayout updateForWidth:size.width];
    }

    // Then, animate alongside the transition, as you would:

    [coordinator animateAlongsideTransition:^
        (id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
            [self.collectionView.collectionViewLayout invalidateLayout];
        }
                                 completion:nil];

    [super viewWillTransitionToSize:size
          withTransitionCoordinator:coordinator];
}

///////////////

@implementation YourLayoutSubclass

- (void)prepareLayout {
    [super prepareLayout];

    [self updateForWidth:self.collectionView.bounds.size.width];
}

- (void)updateForWidth:(CGFloat)width {
    // Update layout as you wish...
}

@end

... See inline code comments above for explanation.

Ben Guild
  • 4,205
  • 5
  • 27
  • 49
0

This solution definitely works.. Try this code

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) 

{

 collectionView.collectionViewLayout.invalidateLayout()

 super.viewWillTransition(to: size, with: coordinator)

}
Dharman
  • 21,838
  • 18
  • 57
  • 107
Amit Thakur
  • 831
  • 9
  • 13
0

Any Changes in UIView must be done from main thread. Put your code of collectionView addition to views hierarchy into:

DispatchQueue.main.async {
    ...  
}
Shree
  • 18,997
  • 28
  • 86
  • 133
0

I've been struggling with this problem for some hours, too.

My UICollectionViewController is embedded in a container view, when rotating the iPad from landscape to portrait it showed the error. The cells did resize though.

I solved the error by calling the following in the UICollectionViewController:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    self.collectionView.collectionViewLayout.invalidateLayout()

    super.viewWillTransition(to: size, with: coordinator)
}

The "super" has to be called after invalidating the layout.

Additionally I needed to call the following in the view where the container view is embedded in:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)

    DispatchQueue.main.async {
        self.collectionViewController.collectionView.reloadData()
    }
}

This way, the cells are updated after the screen rotation is done.

Alessio
  • 2,907
  • 17
  • 32
  • 39
0

In my case, for a vertical UICollectionView I had:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CGSize(width: collectionView.bounds.width, height: padding(scale: 37))
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
    return UIEdgeInsets(top: padding(scale: 10), left:  padding(scale: 4), bottom: padding(scale: 3), right: padding(scale: 4))
}

I fixed it by resting the horizontal insets to the size:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CGSize(width: (collectionView.bounds.width - (2 * padding(scale: 4))), height: padding(scale: 37))
}
Reimond Hill
  • 2,633
  • 21
  • 35
-4

This is what you need:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    //Get frame width
    let width = self.view.frame.width
    //I want a width of 418 and height of 274 (Aspect ratio 209:137) with a margin of 24 on each side of the cell (See insetForSectionAt 24 + 24 = 48). So, check if a have that much screen real estate.
    if width > (418 + 48) {
        //If I do return the size I want
        return CGSize(width: 418, height: 274)
    }else{
        //Get new width. Frame width minus the margins I want to maintain
        let newWidth = (width - 48)
        //If not calculate the new height that maintains the aspect ratio I want. NewHeight = (originalHeight / originalWidth) * newWidth
        let height = (274 / 418) * newWidth
        //Return the new size that is Aspect ratio 209:137
        return CGSize(width: newWidth, height: height)
    }
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
    return UIEdgeInsets(top: 24, left: 24, bottom: 24, right: 24)
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
    return 33
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
    return 33
}
Jon Vogel
  • 4,113
  • 1
  • 28
  • 34