使用UICollectionView,第二部分

甜! 现在,它正在精确地布局我们想要的。 不过,这只是第一步,虽然它是对我们逻辑的可靠验证,但我们绝对没有完成。 接下来,让我们尝试重新排序以进行工作。 我们已经实现了手势识别器,所以让我们开始吧。

好吧,我们学到了两件事。 第一,问题不在于使用过时的布局数据。 第二,我们将在拖动单元格的两秒钟内重新构建整个集合视图布局大约一百万(好,像50)次。 我们可以从中学到什么?

如果您考虑一下,它实际上是完全有意义的。 在释放单元之前,不会通知我们数据源单元已移动,但是我们的布局仍将为周围的单元设置动画。 我们的布局查询委托给定索引路径处的单元格大小,并且该集合是静态的,直到我们删除该单元格为止。 换句话说,如果我们拖拽第五个项目,那么我们的代表将报告第五个项目的大小相同,直到我们放开该单元格为止,尽管集合会在各个单元格之间移动。 如果当我们开始重新排序时,我们节省了所有计算得出的大小,然后在移动单元格时依靠这些大小,该怎么办?

我们可以使用两种方法来完成此操作,但在进行此操作之前,让我们先讨论一下无效上下文。

无效上下文

并非所有失效都是一样的。 就是说,由于种种原因,我们的布局可能会失效,根据我们为什么要使布局失效,我们可能希望做出不同的反应。 UIKit在UICollectionViewLayoutInvalidationContext中捆绑了有关为什么我们在给定时刻失效的信息,我建议在那儿翻阅文档,以查看您可以从中寻求的一切。 它非常强大,在线上似乎不太了解。

当我们拖动单元格时,我们的布局恰巧无效。 每次移动都会触发无效,这解释了为什么尽管只将单元格向左拖动一个项目,却看到了这么多的原因。 我们具体得到的无效上下文是这个非常冗长的方法,它为我们提供了一些附加信息。 当我们重写该方法时,我们期望将上下文传回。 不过,我们确实只想重写它以执行一些与布局有关的额外逻辑,因此,我们将仅返回默认的流程布局实现。

我们还将添加一个新数组来缓存我们的大小,并在prepareLayout中使用它。 请注意,字典将在此处提供更简洁的代码,但是使用数组可以使元素移动得非常非常简单,因为一切都会相应变化。 每当我们的布局在上下文的invalidateDataSourceCounts属性设置为true时无效,或者由于范围更改而无效时,缓存将被清除。 这些新方法是什么样的?

https://gist.github.com/Wailord/93cba52f6549cec72346377c4fe13cf9

我们在那里有一个日志,让我们在清除大小缓存时知道。 每次记录日志时,除非绝对需要,否则我们都应该担心,因为随着视图的发展,调整单元格大小的“自动布局”部分将很快成为最昂贵的部分。 让我们看一下它与移动单元格时的比较。 我们能否做得比50次重新计算更好?

大! 我们根本不需要重新计算,这正是我们所期望的。 从概念上讲,没有理由重新计算单元格,因此我们的实现不需要它是很合适的。 旋转呢?

大! 只需很少的代码,我们已经完成了很多我们想做的事情。 我们的单元正在调整大小,我们可以重新排序并保持大小。 但是,请不要忘记,人们可以对我们的集合执行其他多项操作:插入,更新和删除单元格。 如果我们现在尝试插入怎么办?

尝试插入时发生的崩溃是什么? 我们被告知我们传递了一个nil无效上下文(尽管我们自己没有直接传递该上下文)。 我实际上不确定它的来源是什么-UIKit传递给我们的上下文不是可选的,但是当我们获得它时它为nil(或未初始化)。 现在,我们只处理该调用,并防止我们的超类的实现在nil变为零时进行。

让我们放在一起:

整齐! 它的插入和删除都很漂亮。 我们已经掌握了基础知识; 现在,让我们来一点创意。

动画插入,删除和更新

集合视图(和表格视图)更新动画是我认为iOS开发人员认为理所当然的事情。 它们只是一种“工作”,因此我们无需考虑它们,我们也不需要。 相反,我们可以将注意力集中在最近施加的任何不可能的截止日期上。

集合视图(可按需自定义)具有所有这些行为的钩子。 它使用我们已经了解的相同原理(布局属性),并允许我们在出现单元格和消失单元格时对其进行自定义。 当我们开始以交互方式重新排序单元格时,我们会收到出现的呼叫;放下它时,我们会得到消失的提示。 我们还将收到一个仅用于重新排序的电话,默认情况下,它将使我们的单元格高于其余单元格。

默认情况下, initialLayoutAttributesForAppearingItemAt:返回nil,这意味着要开始的布局属性将与结束帧匹配。 对于finalLayoutAttributesForDisappearingItemAt:也是如此; 默认情况下,它返回nil,因此消失时将没有任何特殊属性。 但是,UICollectionViewFlowLayout具有非零默认值,这是我们获取习惯于所有动画的方式。

更新的生命周期与我们习惯于本文的稍有不同。 插入时,我们将看到以下顺序:

  • UIKit调用的invalidateLayout()
  • 上面的调用导致invalidateLayout(with 🙂
  • 准备()
  • 准备(forCollectionViewUpdates 🙂
  • initialLayoutAttributesForAppearingItem(at :)(x移动多少个单元格)
  • finalizeCollectionViewUpdates()

这里的第一个新手prepareForCollectionViewUpdates是另一种prepare方法,该方法传入一个UICollectionViewUpdateItems数组,每个UICollectionViewUpdateItems指定正在发生的某种更改。 另一个新方法是finalizeCollectionViewUpdates,它在收集所有布局信息之后并且在进行动画之前被调用。 UIKit在动画块内部调用该方法,这意味着我们可以在调用中进行可动画更改。

让我们做一下,这样拖动的单元格在很大程度上是透明的,插入时单元格会长成适当的位置,删除时单元格会缩小为空,并且仅根据边界更改重新计算单元格的高度。 我们还可以将丑陋的尺寸交换代码从invalidationContextForInteractivelyMovingItems […]中移出,并移入它所属的prepare(forCollectionViewUpdates :)方法中。

这是我们现在拥有的更新动画代码:

这是它的样子:

这正是我们想要的! 但是有一个小问题。 例如,当我们调用reloadData时 ,我们并没有抛出旧的大小(我们有点太聪明了)。 其次,当我们调用reloadItems时,我们的大小不会更新尽管清除了我们大小缓存中的适当位置。 为什么是这样?

第一个很简单:我们没有考虑。 reloadData不会抛出“更新”生命周期循环; 它会丢弃旧的集合视图布局并重做所有内容。 但是,我们确实收到了一个invalidateContext(with :)调用,该调用显示“所有内容都无效”,因此我们可以对其进行更新以重置缓存。

第二个技巧更棘手:我们经历了更新生命周期,并在更新后的商品上取消了高度,但是到此为止,我们已经完成了准备工作。 我们最初的准备工作看到的是先前缓存的高度,即旧值,因此不必费心重新计算它。 因此,我们真正需要做的是摆脱prepare(forCollectionViewUpdates :)内部的.reload处理,并尽早取消这些值。 我们不想盲目地抛出尺寸数据,因为我们需要自己对其进行管理。 因此,我在这里选择的方法是创建自己的无效上下文,通过添加reloadingPaths属性扩展流布局的支持范围。 这就引入了一个要求,即每当我插入集合视图时,都需要在更新数据源之后并调用reload之前使用该特定上下文使布局无效。 不是最漂亮的。 没有最糟糕的。

现在,我们应该能够处理这四个案例。 让我们来看看:

我们的单元格可以自行调整大小以适合其内容,可以拖动它们进行重新排序,它们支持自定义插入动画,自定义删除动画,并且可以智能地处理reloadData和reloadItems。 另外,我们可以完成所有这些操作而无需在布局子类之外公开布局代码。 我们的布局子类的时钟线少于250行。 Booyah!实际上,我们的文件都没有超过250个(至少在撰写本文时-我要添加的文档可能会将其翻倒了)。

如果我们想调整一些小东西,我们也会设置好自己。 如果我们希望删除的单元格在缩小和消失时从屏幕上旋转出来怎么办? 如果我们向控制器添加了100万个骇客,那真是个大问题。 但是我们没有。 所以不是。 我们只需要更改finalLayout […]方法即可更改变换和y位置: