使用自动布局在UICollectionView中指定单元的一个维度

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

是否可以为所有单元格的宽度(或高度)指定一个固定的值,并允许其他维度resize?

对于一个简单的例子,考虑一个单元格中的多行标签,将其约束放置在单元格的边上。 多行标签可以调整不同的方式来适应文本。 单元格应填充集合视图的宽度并相应地调整其高度。 相反,单元格的大小是随意的,当单元格大小大于收集视图的不可滚动维度时,它甚至会导致崩溃。


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参数真的只是一个传递两个常量的方法吗? 有没有办法达到我期望得到给定大小的适当高度的行为?


备用解决scheme

1)添加将为单元格指定特定宽度的约束条件可以实现正确的布局。 这是一个糟糕的解决scheme,因为该约束应设置为单元格的集合视图的大小,它没有安全的引用。 这个约束的值可以在单元configuration的时候传入,但是这看起来完全违反直觉。 这也很尴尬,因为直接给单元格添加约束或者它的内容视图导致了很多问题。

2)使用表格视图。 表格视图以这种方式工作,因为单元格具有固定的宽度,但是这不能适应其他情况,例如具有固定宽度单元格的iPad布局。

这听起来像你要求的是一种使用UICollectionView来产生像UITableView的布局的方法。 如果这真的是你想要的,正确的方法是使用自定义的UICollectionViewLayout子类(可能类似于SBTableLayout )。

另一方面,如果你真的问是否有一个干净的方式来做到这一点与默认的UICollectionViewFlowLayout,那么我相信是没有办法的。 即使使用iOS8的自定义单元,也不是简单的。 如你所说,根本的问题是stream程布局的机制没有办法修复一个维度,让另一个维度做出反应。 (另外,即使可以,在需要两次布局传递来确定多行标签的大小方面也会有额外的复杂性,这可能不适合自调整单元如何通过调用systemLayoutSizeFittingSize来计算所有大小。

然而,如果你仍然想创build一个带有stream布局的表格式布局,并且使用确定自己的大小的单元格,并且自然地对集合视图的宽度作出响应,当然这是可能的。 仍然是混乱的方式。 我已经做了一个“尺寸单元”,即一个未显示的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 (ie, 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 } 

(此代码根据“高级收集视图”中WWDC2014会话的示例代码中的苹果示例代码进行调整。)

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

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

我的希望是,最终自我调整的细胞将以不同的方式工作,这将变得更加简单。

示例项目在工作中显示它。

但为什么不使用自我大小的细胞呢?

从理论上讲,iOS8的“自我测量细胞”的新设施应该是不必要的。 如果您已经使用“自动布局”(AL)定义单元格,那么集合视图应该足够聪明,可以让它自己resize并正确放置。 在实践中,我还没有看到任何已经得到这个与多线标签工作的例子。 我认为这部分是因为自我大小的细胞机制仍然是越野车。

但我敢打赌,这主要是因为自动布局和标签的惯用技巧,这就是UILabels需要基本上两步布局的过程。 我不清楚你怎么可以用自我测量的细胞执行两个步骤。

就像我说的,这是一个不同布局的工作。 它是stream布局本质的一部分,它定位具有大小的事物,而不是固定宽度并让他们select它们的高度。

那么preferredLayoutAttributesFittingAttributes呢?

preferredLayoutAttributesFittingAttributes:方法是一个红鲱鱼,我想。 这只是用于新的自我大小细胞机制。 所以只要这个机制是不可靠的,这不是答案。

以及systemlayoutSizeFittingSize:?

你说得对,文档很混乱。

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

实际上,如果设置了UICollectionViewFlowLayoutDelegate.estimatedItemSize ,为了启用新的自定义单元格机制,该值似乎作为targetSize传入。 和UILabel.systemLayoutSizeFittingSize似乎返回完全相同的值作为UILabel.sizeThatFits 。 这是可疑的,因为systemLayoutSizeFittingSize的参数应该是一个粗略的目标,并且sizeThatFits:的参数应该是最大的外接大小。

更多资源

虽然认为这样的例行要求应该需要“研究资源”,但我认为它确实令人伤心。 好的例子和讨论是:

有一个更清洁的方式来做到这一点比这里的其他答案,它运作良好。 它应该是性能(集合视图加载速度快,没​​有不必要的自动布局通行证等),并没有像一个固定的集合视图宽度的任何“幻数”。 更改集合视图大小(例如旋转),然后使布局无效也应该很好。

1.创build以下stream布局子类

 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.注册您的collections视图以自动resize

 // 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, eg 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中执行它 – 水平方式例如(将其高度固定到其集合视图高度):

使用estimatedItemSize初始化Collection视图stream布局以启用自定义单元格:

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

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

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

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

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

尝试在首选布局属性中修改宽度:

 - (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上的东西,它使用了恒定宽度的单元格,并且支持dynamictypes,所以单元格的高度随着系统字体大小的改变而更新。

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

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