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

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

Is it possible to specify a fixed value for the width (or height) of all the cells and allow the other dimensions to resize?

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


IOS8引入了方法 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) Adding constraints that will be specify a specific width for the cell achieves the correct layout. This is a poor solution because that constraint should be set to the size of the cell's collection view which it has no safe reference to. The value for that constraint could be passed in when the cell is configured, but that also seems completely counterintuitive. This is also awkward because adding constraints directly to a cell or it's content view is causing many problems.

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

56397 次浏览

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

Http://www.cocoanetics.com/2013/08/variable-sized-items-in-uicollectionview/
Hope this will be helpful and solve your issue.

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

On the other hand, if you're really asking if there is a clean way to do this with the default UICollectionViewFlowLayout, then I believe there is no way. Even with iOS8's self-sizing cells, it is not straightforward. The fundamental problem, as you say, is that the flow layout's machinery provides no way to fix one dimension and let another respond. (In addition, even if you could, there would be additional complexity around needing two layout passes to size the multi-line labels. This might not fit with how self-sizing cells want to compute all sizing via one call to systemLayoutSizeFittingSize.)

但是,如果您仍然希望创建一个类似于表格视图的布局,其中包含流布局,单元格决定它们自己的大小,并自然地响应集合视图的宽度,那么当然是可能的。还是有混乱的方式。我使用了一个“大小单元格”,也就是一个未显示的 UICollectionViewCell,控制器保留它只是为了计算单元格大小。

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

In your UICollectionViewDelegateFlowLayout, you implement a method like this:

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
}

(此代码改编自“ Advanced Collection View”上 WWDC2014会话的示例代码中的 Apple 示例代码。)

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

Do I like this approach? No!! It feels way too complex, and it does layout twice. But as long as performance doesn't become an issue, I'd rather perform layout twice at runtime than have to define it twice in code, which seems to be the only other alternative.

My hope is that eventually self-sizing cells will work differently and this will all get a lot simpler.

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

但是为什么不使用自我调节单元呢?

理论上,iOS8的“自我尺寸单元”的新设备应该使这一点变得没有必要。如果您已经使用 Auto Layout (AL)定义了一个单元格,那么集合视图应该足够聪明,可以让它自己调整大小并正确地进行布局。在实践中,我还没有看到任何例子,已经得到这与多行标签工作。我认为 这个的部分原因是自我调整细胞机制仍然存在缺陷。

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

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

那么,首选的 LayoutAttributesFittingAttritribute 是什么呢?

我认为 preferredLayoutAttributesFittingAttributes:方法是在转移人们的注意力。这只能与新的自调整单元格机制一起使用。所以只要这个机制不可靠,这就不是答案。

SystemlayoutSizeFittingSize: 又是怎么回事呢?

你说得对,医生让人困惑。

The docs on systemLayoutSizeFittingSize: and systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority: both suggest that you should only pass UILayoutFittingCompressedSize and UILayoutFittingExpandedSize as the targetSize. However, the method signature itself, the header comments, and the behavior of the functions indicate that they are responding to the exact value of the targetSize parameter.

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

更多资源

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

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

- (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上放置的一些东西,它使用固定宽度的单元格,并支持动态类型,因此单元格的高度随着系统字体大小的变化而更新。

一个在 iOS9中用几行代码实现的简单方法——水平方式示例(将其高度固定为 Collection View 高度) :

使用 estimatedItemSize初始化您的集合视图流布局,以启用自调大小单元格:

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:方法,该部分实际上根据您的自动布局约束和刚才设置的高度计算您的动态单元格宽度:

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

这里有一个比其他答案更干净的方法,而且效果很好。它应该是性能(集合视图加载快,没有不必要的自动布局传递等) ,并没有任何“魔术数字”像一个固定的集合视图宽度。改变集合视图的大小,例如在旋转时,然后使布局失效也会很有效。

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. Register your collection view for automatic sizing

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