ChatGPT解决这个技术问题 Extra ChatGPT

使用自动布局在 UICollectionView 中指定一维单元格

在 iOS 8 中,UICollectionViewFlowLayout 支持根据自己的内容大小自动调整单元格的大小。这会根据单元格的内容在宽度和高度上调整单元格的大小。

是否可以为所有单元格的宽度(或高度)指定一个固定值并允许其他尺寸调整大小?

举一个简单的例子,考虑一个单元格中的多行标签,约束将其定位到单元格的两侧。多行标签可以以不同的方式调整大小以适应文本。单元格应填充集合视图的宽度并相应地调整其高度。相反,单元格的大小是随意的,当单元格大小大于集合视图的不可滚动尺寸时,它甚至会导致崩溃。

iOS 8 引入了方法 systemLayoutSizeFittingSize: withHorizontalFittingPriority: verticalFittingPriority: 对于集合视图中的每个单元格,布局在单元格上调用此方法,并传入估计的大小。对我来说有意义的是在单元格上覆盖此方法,传入给定的大小并将水平约束设置为必需,将低优先级设置为垂直约束。这样水平尺寸固定为布局中设置的值,垂直尺寸可以灵活调整。

像这样的东西:

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    UICollectionViewLayoutAttributes *attributes = [super preferredLayoutAttributesFittingAttributes:layoutAttributes];
    attributes.size = [self systemLayoutSizeFittingSize:layoutAttributes.size withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    return attributes;
}

然而,这种方法返回的尺寸完全奇怪。关于这个方法的文档对我来说非常不清楚,并提到使用常量 UILayoutFittingCompressedSize UILayoutFittingExpandedSize ,它只代表一个零大小和一个相当大的大小。

这个方法的 size 参数真的只是传递两个常量的一种方式吗?有没有办法实现我期望为给定尺寸获得适当高度的行为?

替代解决方案

1) 添加将为单元格指定特定宽度的约束可实现正确的布局。这是一个糟糕的解决方案,因为该约束应该设置为它没有安全参考的单元格集合视图的大小。可以在配置单元格时传入该约束的值,但这似乎也完全违反直觉。这也很尴尬,因为直接向单元格或其内容视图添加约束会导致许多问题。

2) 使用表格视图。表格视图开箱即用,因为单元格具有固定宽度,但这不能适应其他情况,例如在多列中具有固定宽度单元格的 iPad 布局。


J
Jagat Dave

听起来您要求的是一种使用 UICollectionView 生成 UITableView 之类的布局的方法。如果这确实是您想要的,那么正确的方法是使用自定义 UICollectionViewLayout 子类(可能类似于 SBTableLayout)。

另一方面,如果你真的在问是否有一种干净的方法可以使用默认的 UICollectionViewFlowLayout 来做到这一点,那么我相信没有办法。即使使用 iOS8 的自调整单元格,也不是很简单。正如您所说,根本问题是流程布局的机制无法固定一个维度并让另一个维度做出响应。 (此外,即使可以,需要两次布局传递来调整多行标签的大小也会带来额外的复杂性。这可能不符合自调整单元格希望通过一次调用 systemLayoutSizeFittingSize 来计算所有大小的方式。)

但是,如果您仍然想创建一个类似 tableview 的布局,带有流式布局,单元格确定自己的大小,并自然地响应集合视图的宽度,当然是可能的。乱七八糟的路还是有的。我已经使用“调整单元格”来完成它,即控制器保留仅用于计算单元格大小的非显示 UICollectionViewCell。

这种方法有两个部分。第一部分是让集合视图委托计算正确的单元格大小,方法是获取集合视图的宽度并使用调整单元格来计算单元格的高度。

在您的 UICollectionViewDelegateFlowLayout 中,您实现了这样的方法:

func collectionView(collectionView: UICollectionView,
  layout collectionViewLayout: UICollectionViewLayout,
  sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
  // NOTE: here is where we say we want cells to use the width of the collection view
   let requiredWidth = collectionView.bounds.size.width

   // NOTE: here is where we ask our sizing cell to compute what height it needs
  let targetSize = CGSize(width: requiredWidth, height: 0)
  /// NOTE: populate the sizing cell's contents so it can compute accurately
  self.sizingCell.label.text = items[indexPath.row]
  let adequateSize = self.sizingCell.preferredLayoutSizeFittingSize(targetSize)
  return adequateSize
}

这将导致集合视图根据封闭的集合视图设置单元格的宽度,然后要求调整大小的单元格计算高度。

第二部分是让尺寸调整单元使用它自己的 AL 约束来计算高度。这可能比它应该做的更难,因为多行 UILabel 的方式实际上需要一个两阶段的布局过程。该工作在方法 preferredLayoutSizeFittingSize 中完成,如下所示:

 /*
 Computes the size the cell will need to be to fit within targetSize.

 targetSize should be used to pass in a width.

 the returned size will have the same width, and the height which is
 calculated by Auto Layout so that the contents of the cell (i.e., text in the label)
 can fit within that width.

 */
 func preferredLayoutSizeFittingSize(targetSize:CGSize) -> CGSize {

   // save original frame and preferredMaxLayoutWidth
   let originalFrame = self.frame
   let originalPreferredMaxLayoutWidth = self.label.preferredMaxLayoutWidth

   // assert: targetSize.width has the required width of the cell

   // step1: set the cell.frame to use that width
   var frame = self.frame
   frame.size = targetSize
   self.frame = frame

   // step2: layout the cell
   self.setNeedsLayout()
   self.layoutIfNeeded()
   self.label.preferredMaxLayoutWidth = self.label.bounds.size.width

   // assert: the label's bounds and preferredMaxLayoutWidth are set to the width required by the cell's width

   // step3: compute how tall the cell needs to be

   // this causes the cell to compute the height it needs, which it does by asking the 
   // label what height it needs to wrap within its current bounds (which we just set).
   let computedSize = self.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)

   // assert: computedSize has the needed height for the cell

   // Apple: "Only consider the height for cells, because the contentView isn't anchored correctly sometimes."
   let newSize = CGSize(width:targetSize.width,height:computedSize.height)

   // restore old frame and preferredMaxLayoutWidth
   self.frame = originalFrame
   self.label.preferredMaxLayoutWidth = originalPreferredMaxLayoutWidth

   return newSize
 }

(此代码改编自 Apple 示例代码,来自 WWDC2014 会议“高级集合视图”的示例代码。)

有几点需要注意。它使用 layoutIfNeeded() 来强制整个单元格的布局,以便计算和设置标签的宽度。但这还不够。我相信您还需要设置 preferredMaxLayoutWidth 以便标签将使用该宽度和自动布局。只有这样,您才能使用 systemLayoutSizeFittingSize 来让单元格计算其高度,同时考虑标签。

我喜欢这种方法吗?不!!感觉太复杂了,而且布局两次。但只要性能不成为问题,我宁愿在运行时执行两次布局,也不愿在代码中定义两次,这似乎是唯一的其他选择。

我希望最终自我调整大小的单元会以不同的方式工作,这一切都会变得简单得多。

Example project 在工作中展示它。

但是为什么不直接使用自定尺寸单元呢?

从理论上讲,iOS8 的“自动调整单元格”的新功能应该让这变得不必要了。如果您使用自动布局 (AL) 定义了一个单元格,那么集合视图应该足够智能,可以让它自行调整大小并正确布局。在实践中,我还没有看到任何可以使用多行标签的示例。我认为 this 部分是因为自调整单元机制仍然存在错误。

但我敢打赌,这主要是因为自动布局和标签通常很棘手,也就是说 UILabel 需要一个基本上两步的布局过程。我不清楚如何使用自定尺寸单元执行这两个步骤。

就像我说的,这确实是一个不同布局的工作。流布局本质的一部分是它定位具有大小的东西,而不是固定宽度并让它们选择它们的高度。

那么preferredLayoutAttributesFittingAttributes: 呢?

我认为 preferredLayoutAttributesFittingAttributes: 方法是一个红鲱鱼。那只能与新的自定尺寸单元机制一起使用。因此,只要该机制不可靠,这不是答案。

systemlayoutSizeFittingSize: 怎么了?

你是对的,文档令人困惑。

systemLayoutSizeFittingSize:systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority: 上的文档都建议您只应将 UILayoutFittingCompressedSizeUILayoutFittingExpandedSize 作为 targetSize 传递。但是,方法签名本身、标头注释和函数的行为表明它们正在响应 targetSize 参数的确切值。

事实上,如果您设置 UICollectionViewFlowLayoutDelegate.estimatedItemSize,为了启用新的自调整单元机制,该值似乎作为 targetSize 传入。 UILabel.systemLayoutSizeFittingSize 似乎返回与 UILabel.sizeThatFits 完全相同的值。这是可疑的,因为 systemLayoutSizeFittingSize 的参数应该是一个粗略的目标,而 sizeThatFits: 的参数应该是最大限制大小。

更多资源

虽然认为这样的常规要求需要“研究资源”令人难过,但我认为确实如此。很好的例子和讨论是:

http://www.objc.io/issue-3/advanced-auto-layout-toolbox.html

http://devetc.org/code/2014/07/07/auto-layout-and-views-that-wrap.html

WWDC2014 会话 232 的代码,“带有集合视图的高级用户界面”


为什么将框架设置回旧框架?
我这样做是因为该函数只能用于确定视图的首选布局大小,而不会影响您调用它的视图的布局状态。实际上,如果您仅出于以下目的而持有此视图,则这无关紧要做布局计算。另外,我在这里所做的还不够。单独恢复框架并不能真正将其恢复到以前的状态,因为执行布局可能会修改子视图的布局。
对于自 ios6 以来人们一直在做的事情来说,这真是一团糟。苹果总是以某种方式出现,但它从来都不是完全正确的。
您可以在 viewDidLoad 中手动实例化大小调整单元格,并在其中查看自定义属性。它只有一个细胞,所以很便宜。由于您只是将它用于调整大小,并且您正在管理与它的所有交互,因此它不需要参与集合视图的单元重用机制,因此您不需要从集合视图本身获取它。
到底怎么能自定尺寸的细胞仍然被打破?!
J
Jordan Smith

与此处的其他一些答案相比,有一种更清洁的方法可以做到这一点,而且效果很好。它应该是高性能的(集合视图加载速度快,没有不必要的自动布局传递等),并且没有像固定集合视图宽度那样的任何“神奇数字”。更改集合视图大小,例如在旋转时,然后使布局无效也应该很好用。

1.创建如下流布局子类

class HorizontallyFlushCollectionViewFlowLayout: UICollectionViewFlowLayout {

    // Don't forget to use this class in your storyboard (or code, .xib etc)

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = super.layoutAttributesForItemAtIndexPath(indexPath)?.copy() as? UICollectionViewLayoutAttributes
        guard let collectionView = collectionView else { return attributes }
        attributes?.bounds.size.width = collectionView.bounds.width - sectionInset.left - sectionInset.right
        return attributes
    }

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let allAttributes = super.layoutAttributesForElementsInRect(rect)
        return allAttributes?.flatMap { attributes in
            switch attributes.representedElementCategory {
            case .Cell: return layoutAttributesForItemAtIndexPath(attributes.indexPath)
            default: return attributes
            }
        }
    }
}

2. 注册您的收藏视图以自动调整大小

// The provided size should be a plausible estimate of the actual
// size. You can set your item size in your storyboard
// to a good estimate and use the code below. Otherwise,
// you can provide it manually too, e.g. CGSize(width: 100, height: 100)

flowLayout.estimatedItemSize = flowLayout.itemSize

3. 在您的单元格子类中使用预定义的宽度 + 自定义高度

override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    layoutAttributes.bounds.size.height = systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
    return layoutAttributes
}

这是我在自定尺寸主题上找到的最好的答案。谢谢乔丹!
无法让它在 iOS (9.3) 上运行。在 10 上它工作正常。应用程序在 _updateVisibleCells 上崩溃(无限递归?)。 openradar.me/25303115
@cristiano2lopes 它适用于我在 iOS 9.3 上。确保您提供了一个合理的估计,并确保您的单元格约束设置为解析为正确的大小。
这效果惊人。我正在使用 Xcode 8 并在 iOS 9.3.3(物理设备)中进行了测试,它不会崩溃!效果很好。只要确保你的估计是好的!
@JordanSmith 这在 iOS 10 中不起作用。你有任何示例演示。我已经尝试了很多事情来根据其内容来做集合视图单元格的高度,但没有。我在 iOS 10 中使用自动布局并使用 swift 3。
T
Tanguy G.

在 iOS 9 中通过几行代码实现它的简单方法 - 水平方式示例(将其高度固定到其 Collection View 高度):

使用 estimatedItemSize 初始化您的 Collection View Flow Layout 以启用自调整单元格:

self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.estimatedItemSize = CGSizeMake(1, 1);

实现集合视图布局委托(大部分时间在您的视图控制器中),collectionView:layout:sizeForItemAtIndexPath:。这里的目标是将固定高度(或宽度)设置为 Collection View 维度。 10 值可以是任何值,但您应该将其设置为不破坏约束的值:

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

覆盖您的自定义单元格 preferredLayoutAttributesFittingAttributes: 方法,这部分实际上根据您的自动布局约束和您刚刚设置的高度计算您的动态单元格宽度:

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
    UICollectionViewLayoutAttributes *attributes = [layoutAttributes copy];
    float desiredWidth = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].width;
    CGRect frame = attributes.frame;
    frame.size.width = desiredWidth;
    attributes.frame = frame;
    return attributes;
}

您是否知道在 iOS9 上,当单元格包含多行 UILabel 时,这种更简单的方法是否可以正常工作,其换行会影响单元格的固有高度?这是一个常见的情况,过去需要我描述的所有后空翻。我想知道自我回答以来iOS是否已修复它。
@algal 遗憾的是,答案是否定的。
D
Daniel Galasko

尝试在首选布局属性中固定宽度:

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    UICollectionViewLayoutAttributes *attributes = [[super preferredLayoutAttributesFittingAttributes:layoutAttributes] copy];
    CGSize newSize = [self systemLayoutSizeFittingSize:CGSizeMake(FIXED_WIDTH,layoutAttributes.size) withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    CGRect newFrame = attr.frame;
    newFrame.size.height = size.height;
    attr.frame = newFrame;
    return attr;
}

当然,您还希望确保正确设置布局以:

UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *) self.collectionView.collectionViewLayout;
flowLayout.estimatedItemSize = CGSizeMake(FIXED_WIDTH, estimatedHeight)];

这是我放在 Github 上的东西,它使用恒定宽度的单元格并支持动态类型,因此单元格的高度会随着系统字体大小的变化而更新。


在此代码段中,您调用 systemLayoutSizeFittingSize:。您是否设法在没有在某处设置 preferredMaxLayoutWidth 的情况下使其工作?根据我的经验,如果您没有设置preferredMaxLayoutWidth,那么您需要进行额外的手动布局,例如,通过覆盖sizeThatFits: 的计算来重现其他地方定义的自动布局约束的逻辑。
@algal preferredMaxLayoutWidth 是 UILabel 上的属性吗?不完全确定这有什么关系?
哎呀!好点,不是!我从来没有用 UITextView 处理过这个,所以没有意识到他们的 API 在那里不同。似乎 UITextView.systemLayoutSizeFittingSize 尊重它的框架,因为它没有内在内容大小。但是 UILabel.systemLayoutSizeFittingSize 忽略了框架,因为它有一个 intrinsicContentSize,所以需要 preferredMaxLayoutWidth 来获取 intrinsicContentSize 以将其包裹高度暴露给 AL。似乎 UITextView 仍然需要两次传递或手动布局,但我认为我并不完全理解这一切。
@algal 我同意,我在使用 UILabel 时也遇到了一些麻烦。设置它的首选宽度解决了这个问题,但最好有一个更动态的方法,因为这个宽度需要随着它的超级视图缩放。我的大多数项目都不使用首选属性,我通常将其用作快速修复,因为总是存在更深层次的问题
D
Dhaivat Vyas

是的,它可以通过编程方式使用自动布局并通过在情节提要或 xib 中设置约束来完成。您需要为宽度大小添加约束以保持不变并将高度设置为大于或等于。
http://www.thinkandbuild.it/learn-to-love-auto-layout-programmatically/

http://www.cocoanetics.com/2013/08/variable-sized-items-in-uicollectionview/
希望这会有所帮助并解决您的问题。


这应该适用于绝对宽度值,但如果您希望单元格始终是集合视图的全宽,它将不起作用。
@MichaelWaterfall 我设法使用 AutoLayout 让它以全宽工作。我在单元格内的内容视图上添加了宽度约束。然后在 cellForItemAtIndexPath 中更新约束:cell.constraintItemWidth.constant = UIScreen.main.bounds.width。我的应用程序仅适用于肖像。您可能希望在方向更改后 reloadData 以更新约束。