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


113

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

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

对于一个简单的示例,请考虑单元格中的多行标签,将其放置在单元格的侧面有约束。多行标签可以用不同的方式调整大小以容纳文本。单元格应填充集合视图的宽度,并相应地调整其高度。而是随意调整单元格的大小,并且当单元格大小大于集合视图的不可滚动尺寸时,甚至会导致崩溃。


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

像这样:

- (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布局中具有固定宽度的单元格在多列中。

Answers:


135

听起来您想要的是一种使用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有效地需要两阶段布局过程,因此这可能比应该做的难。工作是在method中完成的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
 }

(此代码改编自WWDC2014会话“高级收藏夹视图”上的示例代码中的Apple示例代码。)

有几点需要注意。它使用layoutIfNeeded()强制整个单元格的布局,以便计算和设置标签的宽度。但这还不够。我相信您还需要进行设置,preferredMaxLayoutWidth以便标签将在“自动版式”中使用该宽度。然后只有systemLayoutSizeFittingSize在考虑标签的情况下,才能使用该单元来计算单元的高度。

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

我的希望是,最终自动调整大小的单元将以不同的方式工作,而这一切将变得更加简单。

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

但是,为什么不只使用自动调整大小的单元呢?

从理论上讲,iOS8的“自定义单元格”的新功能应使其不必要。如果您使用自动布局(AL)定义了一个单元格,则该集合视图应该足够聪明,以使其能够自行调整大小并正确地进行布局。在实践中,我还没有看到任何示例可将其用于多行标签。我认为部分是因为自调整大小的细胞机制仍是越野车。

但我敢打赌,这主要是因为“自动布局”和标签通常很棘手,这就是UILabel需要一个基本的两步布局过程。对我而言,目前尚不清楚如何使用自定义大小的单元格执行两个步骤。

就像我说的,这确实是另一种布局的工作。流布局本质的一部分是放置具有大小的对象,而不是固定宽度并让他们选择高度。

那么,preferredLayoutAttributesFittingAttributes:呢?

preferredLayoutAttributesFittingAttributes:我认为该方法是一种红色鲱鱼。只有新的自动调整大小单元机制才能使用该功能。因此,只要该机制不可靠,这不是答案。

以及systemlayoutSizeFittingSize:是怎么回事?

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

上Docs systemLayoutSizeFittingSize:systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority:两个建议,你应该只通过UILayoutFittingCompressedSizeUILayoutFittingExpandedSize作为targetSize。但是,方法签名本身,标题注释和功能的行为表明它们正在响应targetSize参数的确切值。

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

更多资源

而可悲的是认为这样的常规要求应要求“的科研资源”,我认为它。很好的例子和讨论是:


为什么将框架重新设置为旧框架?
vrwim 2015年

这样做是为了使该函数只能用于确定视图的首选布局大小,而不会影响调用它的视图的布局状态。实际上,如果仅出于以下目的而持有该视图,则这无关紧要。做布局计算。另外,我在这里所做的还不够。单独还原框架并不能真正将其还原到以前的状态,因为执行布局可能会修改子视图的布局。
algal 2015年

1
自从ios6以来人们一直在做的事情真是一团糟。Apple总是以某种方式出现,但并非总是正确的。
GregP

1
您可以在viewDidLoad中手动实例化大小调整单元格,并在其中保持查看自定义属性。它只有一个电池,所以很便宜。由于您仅使用它进行大小调整,并且要管理与之的所有交互,因此它不需要参与集合视图的单元重用机制,因此不需要从集合视图本身获取它。
藻类

1
自定义大小的细胞到底又怎么被打破?
Reuben Scratton '16

50

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

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
}

8
这是我在自我调整主题上找到的最大答案。谢谢乔丹!
haik.ampardjian '16

1
无法在iOS(9.3)上使用。在10上工作正常。应用程序在_updateVisibleCells上崩溃(无限递归?)。openradar.me/25303115
cristiano2lopes

@ cristiano2lopes在iOS 9.3上对我来说效果很好。确保您提供了合理的估计,并确保将单元格约束设置为解析为正确的大小。
乔丹·史密斯

这很棒。我正在使用Xcode 8,并在iOS 9.3.3(物理设备)中进行了测试,并且不会崩溃!效果很好。只要确保您的估计是正确的!
哈密​​德(Hahmed),2016年

1
@JordanSmith在iOS 10中不起作用。您有任何示例演示。我已经尝试了很多事情来根据其内容来收集视图单元格的高度,但是什么也没有做。我在iOS的10使用自动布局和使用SWIFT 3
的Ekta Padaliya

6

在iOS 9中用几行代码即可完成此操作的简单方法-水平方式即为示例(将其高度固定为其Collection View的高度):

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

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

(通常在您的View Controller中)实现Collection View Layout Delegate 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;
}

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

2
@algal可悲的是,答案是否定的。
jamesk

4

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

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

@algal preferredMaxLayoutWidth是UILabel的属性吗?不完全确定这有什么关系?
Daniel Galasko 2014年

糟糕!好点,不是!我从来没有使用UITextView来处理这个问题,所以没有意识到他们的API在那里有所不同。好像UITextView.systemLayoutSizeFittingSize尊重它的框架,因为它没有internalContentSize。但是UILabel.systemLayoutSizeFittingSize会忽略该框架,因为它具有internalContentSize,因此需要preferredMaxLayoutWidth才能获取internalContentContentSize,以将其包装高度显示给AL。看起来UITextView仍然需要两次通过或手动布局,但是我认为我并不完全理解这一切。

@algal我可以同意,在使用UILabel时我也遇到了一些毛病。设置其首选宽度可以解决此问题,但是最好采用一种更具动态性的方法,因为该宽度需要随其父视图缩放。我的大多数项目都没有使用首选属性,我通常将其用作快速解决方法,因为总会有更深的问题
Daniel Galasko 2014年

0

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

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


3
这应该适用于绝对宽度值,但是在您希望单元格始终是集合视图的完整宽度的情况下,它将不起作用。
2014年

@MichaelWaterfall我设法使用AutoLayout使它以全宽工作。我在单元格中的内容视图上添加了宽度约束。然后,在cellForItemAtIndexPath更新的约束:cell.constraintItemWidth.constant = UIScreen.main.bounds.width。我的应用程序仅可移植。您可能希望reloadData在更改方向后更新约束。
Flitskikker,2016年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.