UICollectionViewFlowLayout 的行为未定义,因为单元格宽度大于 collectionView 宽度

the behavior of the UICollectionViewFlowLayout is not defined, because the cell width is greater than collectionView width

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 <UICollectionViewFlowLayout: 0x7f24d670>, and it is attached to <UICollectionView: 0x7b377200; frame = (0 0; 768 1024); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x7f24ce30>; animations = { position=<CABasicAnimation: 0x7f279d60>; bounds.origin=<CABasicAnimation: 0x7f279430>; bounds.size=<CABasicAnimation: 0x7f279480>; }; layer = <CALayer: 0x7aec9cf0>; contentOffset: {0, 0}; contentSize: {1024, 770}> collection view layout: <UICollectionViewFlowLayout: 0x7f24d670>. 2015-08-18 16:07:51.524 Example[16070:269647] Make a symbolic breakpoint at UICollectionViewFlowLayoutBreakForInvalidSizes to catch this in the debugger.

这就是我得到的和我所做的

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

我从横向旋转到纵向,然后控制台仅在 iOS9 中显示此错误消息,如果有人知道发生了什么并且是否有解决此问题的方法?

我遇到了类似的问题。

在我的例子中,我有一个集合视图,当您点击其中一个单元格时,会打开一个带有 UITextField 的弹出窗口,以编辑该项目。在弹出窗口消失后,self.collectionView.contentInset.bottom 被设置为 55(最初为 0)。

为了解决我的问题,弹出视图消失后,我手动将 contentInset 设置为 UIEdgeInsetsZero

最初的问题似乎与显示在键盘顶部的上下文预测栏有关。隐藏键盘后,横条消失,但contentInset.bottom值并没有恢复到原来的值。

由于您的问题似乎与单元格的宽度而非高度有关,请检查 contentInsetlayout.sectionInset 值是否与您设置的值相同.

这是因为你的集合视图单元格的宽度大于集合视图宽度 rotation.suppose 你有一个 1024x768 屏幕并且你的集合视图填充 screen.When 你的设备是横向的,你的单元格的宽度将是 self.collectionView.frame.size.width - 20 =1004 并且它大于纵向集合视图的宽度 = 768.The 调试器说 "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".

我正在使用 UICollectionViewFlowLayout 的子 class 并使用其 itemSize 属性 指定单元格大小(而不是 collectionView:sizeForItemAtIndexPath: 委托方法).每次我将屏幕旋转到较短的宽度(例如横向 -> 纵向)时,我都会收到这个巨大的警告。

我可以通过执行 2 个步骤来修复它。

第一步:UICollectionViewFlowLayout子class的prepareLayout()函数中,将super.prepareLayout()移动到after 其中设置了 self.itemSize。我认为这使得超级 class 使用正确的 itemSize 值。

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()
  }

  // ...

}

请注意,当屏幕旋转停止工作时,上述更改会以某种方式使布局大小发生变化。这是第 2 步的用武之地。

第 2 步:将其放入保存集合视图的 视图控制器

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

现在警告消失了:)


一些注意事项:

  1. 确保您正在向 collectionView 添加约束,而不是使用 collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

  2. 确保您没有在 viewWillTransitionToSize() 中调用 invalidateLayout,因为横向的边到边单元格的宽度大于纵向的集合视图的框架宽度.请参阅以下参考资料。

参考资料

当您的集合视图调整到较窄的宽度(例如,从横向模式变为纵向模式)并且单元格变得太大而无法容纳时,就会发生这种情况。

为什么单元格变得太大,因为应该调用集合视图流布局并且 return 合适的大小?

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

更新以包含 Swift 4

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

这是因为这个函数没有被调用,或者至少没有立即被调用。

发生的情况是您的集合视图流布局子类没有覆盖 shouldInvalidateLayoutForBoundsChange 函数,默认情况下 returns false

当此方法 return 为 false 时,集合视图首先尝试使用当前单元格大小,检测到问题(记录警告),然后调用流布局调整单元格大小。

这意味着两件事:

1 - 警告本身并无害处

2 - 您可以通过简单地将 shouldInvalidateLayoutForBoundsChange 函数重写为 return true 来摆脱它。 在这种情况下,当集合视图边界发生变化时,将始终调用流布局。

如果 collectionView.contentOffset 更改为负数,您可以在调试器中检查,在我的例子中它从 (0,0) 更改为 (0,-40)。您可以使用此方法解决此问题

if ([self respondsToSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)]) {
   self.automaticallyAdjustsScrollViewInsets = NO;
}

经过一些试验后,这似乎也与您如何布局 collectionView 相关。

tl;dr 是:使用 AutoLayout,而不是 autoresizingMask。

因此,对于问题的核心,我发现处理方向变化的最佳解决方案使用以下代码,这些代码都很有意义:

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)
}

但是,在某些情况下您仍然会收到商品尺寸警告。对我来说,如果我在一个选项卡中处于横向状态,切换到另一个选项卡,旋转到纵向,然后 return 到上一个选项卡。我尝试在 willAppear、willLayout 和所有常见的嫌疑人中使布局无效,但没有成功。事实上,即使你在 invalidateLayout 之前调用 super.willAppear() 你仍然会收到警告。

最终,这个问题与 collectionView 边界的大小有关,在代理被要求提供 itemSize 之前更新。

考虑到这一点,我尝试使用 AutoLayout 而不是 collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight],问题就解决了! (我使用 SnapKit 进行 AutoLayout,这样我就不会经常拉头发)。您还只需要 viewWillTransition 中的 invalidateLayout(没有 coordinator.animate,就像您在示例中所做的那样),以及 [=18= 底部的 invalidateLayout ]. 似乎涵盖所有情况。

我不知道您是否在使用自动调整大小 - 知道我的 theory/solution 是否适用于所有人会很有趣。

我遇到了同样的问题。我通过清除其约束的集合视图并在 Storyboard 中重置它们来解决此问题。

这是您需要的:

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
}

我已经使用 safeAreaLayoutGuide 解决了这个问题。

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

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

并且您还必须覆盖此函数才能正确支持纵向和横向模式。

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

这对我有用:

旋转布局无效:

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

有单独的计算可用宽度的函数:

注意:考虑到部分插图和内容插图。

// 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
}

然后当然最后返回项目大小:

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

我认为重要的是要注意这是有效的,因为使布局无效不会立即触发单元格大小的重新计算,而是在下一个布局更新周期内。这意味着一旦项目大小回调最终被调用,集合视图就会有正确的框架,从而允许准确调整大小。

瞧!

当我们将 estimatedItemSize 设置为 automaticSize 并且单元格的计算大小小于 50x50 时,我也看到过这种情况(注意:此答案在 iOS 12 时有效。也许以后版本已经修复了)。

即如果您通过将估计的项目大小设置为 automaticSize 常量来声明对自调整单元格的支持:

flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

并且您的单元格实际上小于 50x50 点,然后 UIKit 会打印出这些警告。 FWIW,单元格最终大小合适,警告似乎无害。

一个 fix 解决方法是用 1x1 估计项目大小替换常量:

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

这不会影响自动调整大小的支持,因为 estimatedItemSize 的文档提到:

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

但是,它 might/might-not 对性能有影响。

我发现这对 UICollectionViewController 非常有效并且动画正确:

- (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

... 请参阅上面的内联代码注释以获取解释。

我可以通过把 super.prepare() 在结束时 override func prepare() { }

这个解决方案绝对有效..试试这个代码

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

{

 collectionView.collectionViewLayout.invalidateLayout()

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

}

将您的集合视图的估计大小 'None' 值从自动。

您可以通过故事板或以编程方式执行此操作。

但你必须使用自定义布局手动计算 class 或 'UICollectionViewDelegateFlowLayout' 协议 API

对于Xcode 11,上述方法中的none对我有用。

原来需要在集合视图大小面板中将估计大小设置为 None。

参见:https://forums.developer.apple.com/thread/123625

select 故事板中的集合视图转到尺寸检查器将估计尺寸更改为 none

UIView 中的任何更改都必须从主线程完成。将您的 collectionView 代码添加到视图层次结构中:

DispatchQueue.main.async {
    ...  
}

我也为这个问题苦苦挣扎了几个小时。

我的 UICollectionViewController 嵌入在容器视图中,将 iPad 从横向旋转到纵向时显示错误。虽然单元格确实调整了大小。

我通过在 UICollectionViewController 中调用以下命令解决了错误:

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

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

"super" 必须在 之后 使布局无效。

此外,我需要在容器视图嵌入的视图中调用以下命令:

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

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

这样,屏幕旋转完成后单元格就会更新。

XCode 11

对我来说,原因是 CollectionView 的估计选项大小设置为自动。 这似乎是 XCode 11.

中的默认行为

因此对我来说,我将图像加载到单元格中,因为图像的大小和自动选项(想要根据图像的大小调整单元格的大小),单元格的宽度变得更大比 CollectionView 的宽度。

通过将此选项设置为 None 问题已解决。

就我而言,对于垂直 UICollectionView,我有:

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))
}

我通过将水平插图调整为以下大小来修复它:

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))
}

这是我的解决方案:

  1. 从视图控制器中删除所有 UICollectionViewFlowDelegate 方法。

  2. 创建您自己的 flowLayout class 作为子class UICollectionViewFlowLayout。

    class ImageCollectionViewFlowLayout: UICollectionViewFlowLayout {}

  3. 将所有大小和其他参数配置到 prepare() 方法中。请参阅下面的示例。

     private let sectionInsets = UIEdgeInsets(top: 50.0, left: 20.0, bottom: 50.0, right: 20.0)
    
     override func prepare() {
         super.prepare()
    
         guard let collectionView = collectionView else { return }
         self.sectionInset = sectionInsets
         self.minimumLineSpacing = sectionInsets.bottom
         self.minimumInteritemSpacing = sectionInsets.bottom
         let width = collectionView.bounds
             .inset(by: collectionView.layoutMargins)
             .inset(by: self.sectionInset)
             .width
         self.estimatedItemSize = CGSize(width: width, height: width + 16)
         //self.itemSize - I do not set itemsize because my cell has dynamical height
     }
    
  1. 在故事板(或 xib)上设置新的 class 集合视图。

就是这样。保持代码整洁。