自定义UICollectionViewLayout自动布局和动态类型

Gousto的iOS团队对其所有UI使用自动布局,因此我们能够容纳可变内容,动态类型和不同大小级别的设备。 当我们想将食谱列表切换到中等规模的设备上的网格时,我们很难找到有关如何使用UICollectionView和用于UICollectionViewCells的XIB实现此目的的任何文档。 这篇博客文章讨论了我们遇到的问题以及如何使用自定义UICollectionViewLayout实现它。

为什么?

自从引入动态类型以来,我们认为支持它对我们的用户将是有益的(这意味着响应用户在其设备上设置的字体比例)。 当要求重新设计我们的主要配方列表以添加另一种具有不同高度的电池时,我们认为这是一个绝佳的机会,应该不会太痛苦。 苹果公司说“在开始构建自定义布局之前,请考虑这样做是否真的必要”,这也总是很棒。

在整个这篇文章中,我将展示一些代码,但是我还将附加一个演示项目,以便您可以看到它的工作!

固定约束

以前,我们的单元格都具有相同的高度,而我们的食谱标题标签都具有高度限制,且字体大小最小,因此字体大小会缩小以容纳文本,在极端情况下,标题会被截断。 我们要做的第一件事是删除所有不必要的约束并使所有变量可变。 我们拥有的单元格布局非常复杂,带有许多子视图(有时是不必要的),因此我们对此进行了整理,并尝试使其尽可能平坦,并针对不同的场景使用了不同的单元格。

介绍自定义布局

如果要创建自定义布局,则基本上必须自己做好所有事情,这可能就是Apple不真正推荐它的原因。 在布局中,有三种主要方法:

func prepare()

最初,我认为该方法一开始只会被调用一次,但是它被频繁调用,因此,如果layoutAttributesForItems为空(这是我们用于单元的缓存),我们只想计算估计的单元大小。 第一次调用时,我们的缓存为空,因此我们创建了初始布局。

在initialLayout中,我们实际上将执行我们的像元大小估计。 首先确定我们需要多少列,因为根据窗口的宽度,我们有1、2或3。

上面的代码创建了两个数组,对于我们的X和Y单元格位置,我们仅用列数的位置(例如三列)来初始化y数组

  yOffset = [0,0,0] 

我们基于列再次完成xOffset数组,因此如果我们有一个1024像素宽度的设备和三列,我们将拥有

  xOffset = [0,341,682,0,341,682,…],依此类推。 

接下来,我们需要遍历每个单元并为每个单元创建布局属性,并将其存储在本地缓存中。

在创建框架的最后,我们需要添加下一个yPosition,以便该列下面的单元格知道它的开始位置。 我们还将为下一遍设置该列。 最后,我们需要用新的高度更新contentHeight属性。

  contentHeight + = collectionView.contentInset.bottom 

var collectionViewContentSize:CGSize

这只是返回我们在prepare方法中刚刚计算出的collectionView内容区域的大小。

func preferredLayoutAttributesFitting(_ layoutAttributes:UICollectionViewLayoutAttributes)-> UICollectionViewLayoutAttributes

这可能是使布局正常工作的最困难的部分。 我们发现没有多少文档对我们有帮助,因此为此进行了大量的尝试和错误。

在准备好布局之后,我们像往常一样将一个单元出队,并会自动调用preferredLayoutAttributesFitting。 这是单元格有机会指示其首选属性(包括尺寸)的位置,我们使用自动布局来计算这些属性。

我们将垂直fittingSize设置为压缩,因为我们希望像元在满足其约束的情况下尽可能达到最小高度。 该方法的关键是systemLayoutSizeFitting…因为我们的collectionView垂直滚动,所以我们可以将水平优先级设置为.required-这意味着它只能与预先计算的宽度一样大。 垂直优先级设置为.defaultLow,因为我们需要单元能够增长。 最初,我们弄错了这一点,最终导致一些单元会增长到1000像素……

现在,布局将调用invalidate,在其中我们检查自originalAttributes之后单元格的高度是否已更改。 如果没有的话,我们会忽略它,但是如果有……

func invalidationContext(forPreferredLayoutAttributes preferredAttributes:UICollectionViewLayoutAttributes,withOriginalAttributes originalAttributes:UICollectionViewLayoutAttributes)-> UICollectionViewLayoutInvalidationContext

在这里,我们需要计算出单元格高度之间的差异,并使用新的高度更新属性。

如果我们更新列中的一个单元格,则需要调整该列中所有其他单元格的Y位置并使其无效,以便我们遍历所有单元格。

调整完所有单元格的高度后,我们需要使用最后一行单元格中的layoutAttributes将collectionView的整个高度设置为最高列的高度。

我们学到了什么?

有些地方缺少文档,主要是单元格中的preferredLayout方法,这是布局中最重要的部分之一。

如果我们没有尝试支持动态类型,那么任务会容易得多,我们必须更改很多约束,而且确实很麻烦。 现在,我们的单元格完全是通过xibs中的自动布局来计算的,我们真的没有发现任何人在网上做与我们相同的事的例子,人们倾向于根据代码中标签的高度来计算单元格的高度。 我们不想这样做,因为我们的单元很复杂,这样做会带来可维护性的负担。

同样重要的是要认识到诸如prepare之类的方法被称为很多方法(加载集合视图时,用户滚动时等),因此您必须确保保留数据缓存并仅在必要时重新计算它,或者相关部分或仅在需要时使用。

另一件事是避免一次更改太多的内容,否则将很难弄清楚什么在起作用,什么在不起作用。

示范项目

如果您想看一下,这是演示项目的链接!

Gousto / UICollectionViewLayoutDemo
人们可以在GitHub上构建软件。 超过2千8百万的人使用GitHub来发现,发掘和贡献超过 github.com

Kiera O’Reilly
软件工程师