LiveCollections第4部分:轮播表

这是一个足够具体的案例,我认为它值得保留自己的内容。 如果您曾经使用过内容驱动型应用程序(例如Scribd),在其中选择了许多令人兴奋的个性化类别,那么您以前可能已经与该结构进行了交互。

如前所述,在Scribd中,您在应用程序中看到的每个类别都是UITableView的一行,而水平轮播是UICollectionView 。 这导致听起来与情况3和4相似的情况,但是增加了一些复杂性。

主要区别在于我们将动画应用于需要协调的两种不同类型的视图对象。 这种安排的部分技巧是我们不想执行UITableView重新加载操作。 即使您传递animationStyle = .none ,该单元仍然可以闪烁。 更糟糕的是,重新加载单元格会导致出队操作,因此在重建单元格时,请告别任何不错的UICollectionView操作。 另一个要考虑的因素是,我们要确保基础表格视图在考虑其自己的动画完成之前,等待所有大学视图动画完成。

方案7:轮播表

在这种情况下,我们需要解决的主要问题是我们有一个UITableView和N个UICollectionViews ,我们需要它们以协调的方式进行动画处理。 也就是说,我们希望能够对表格视图行上的插入,删除和移动进行动画处理,但是对于任何重新加载,我们都不希望表格视图来处理它,而是触发集合视图动画。

为此,我们首先创建一个新的数据结构,如下所示:

  struct CarouselRow:相等{ 
let标识符:字符串
放电影:[电影]
}扩展名CarouselRow:唯一标识{
typealias RawType = CarouselRow
var uniqueID:字符串{返回标识符}
var hashValue:Int {
返回movies.reduce(identifier.hashValue){$ 0 ^ $ 1.hashValue}
}
}

由于我们没有使用多个部分,因此我们的CarouselRow结构只需要符合UniquelyIdentifiable ,并且可以使用CollectionData更新表视图。 还要注意,它包含[Movie]数组,该数组符合UniquelyIdentifiable 。 这将传递给一个子控件,该子控件将控制UICollectionView并将使用CollectionData保留其自己的数据。

那么,我们如何协调所有这些呢?

面临的挑战是让拥有UITableVIew主控制器在正确的时间更新每个子控制器,但是如何?

这将通过两种关键方式进行处理:

  1. 表格视图和集合视图的所有数据更新将首先通过CollectionDatacollectionData.update(carouselRows)通过主表格视图控制器。
  2. 主表视图控制器将采用委托CollectionDataManualReloadDelegate ,该代理将为其提供更新子控制器所需的定时挂钩。

这是我们将在CollectionDataManualReloadDelegate使用的两个相关函数:

  func willHandleReload(在indexPath:IndexPath)-> Bool 
func reloadItems(在indexPaths:[IndexPath],indexPathCompletion:@转义(IndexPath)->无效)

第一种方法通知表格视图,委托打算照顾indexPath的重载,并将其从动画批处理中删除。

第二种方法告诉我们何时可以安全地为每个indexPath重新加载。 这是一个例子:

  func willHandleReload(在indexPathPair:IndexPathPair)-> Bool { 
返回true //处理所有重载
} func reloadItems(位于indexPaths:[IndexPath],indexPathCompletion:@escaping(IndexPath)->无效)){
indexPaths.forEach {
让数据= collectionData [indexPath.item]
让dataSource = carouselDataSource(for:data.identifier)
让itemCompletion = {indexPathCompletion(indexPath)}
dataSource.update(带有:data.movi​​es,完成:itemCompletion)
}
}

正如您将看到的,我们只需要获取正确索引处的数据并将其传递给子控制器dataSource ,对于发送给我们的每个indexPath,我们将触发一次完成块。

注意:触发发送到函数的每个 indexPath的完成功能是非常重要的。 如果您不这样做,那么表视图动画将无限期等待。

此外,由于CollectionData保持所有操作有序且线程安全,即使我们在上一个动画完成之前已经对其传递了另一个更新,我们仍然可以直接从CollectionData对象请求数据,并且该数据将与动画同步。

但是,等等,您完全跳过了UITableViewCells是可淘汰的事实……

您的观察非常机敏。 荣誉

所有这一切的另一个复杂性是,由于我们在可出队的单元内对UICollectionViews进行了动画UICollectionViews ,因此我们冒着极端情况的风险,即我们将不同的数据源指向UICollectionViews中间动画,因此我们可以从重用池中获得视图仍然指向数据源,或者有两个CollectionData对象临时指向同一视图。

是的,听起来并不好,这些都是严重的问题。

为了处理这些情况,将需要结合使用一种附加的委托协议和在适当的位置应用一些防御性代码。 让我们来看看。

通过采用CollectionDataReusableViewVerificationDelegate协议(模糊而神秘),您可以通知动画视图它是否仍指向有效数据源。 代码如下:

  collectionData.validationDelegate = self ... func isDataSourceValid(用于视图:DeltaUpdatableView)-> Bool { 
守护让collectionView = view as? UICollectionView,
collectionView.delegate ===自我,
collectionView.dataSource ===自身else {
返回假
返回true
}

其次,我们需要保护数据中任何基于索引的查找,以防止出现越界错误。 如果表视图在重用池中,但仍在尝试完成其动画,则会发生这种情况。 我们不需要该视图做任何明智的事情,我们只需要防止它引发任何错误代码即可。

任何需要进行索引查找的UICollectionViewDataSource/Delegate方法(例如let movie = collectionData[indexPath.item]都应受到保护。

您可以执行以下操作:

  func collectionView(_ collectionView:UICollectionView,cellForItemAt indexPath:IndexPath)-> UICollectionViewCell { 

让单元格= collectionView.dequeueReusableCell(withReuseIdentifier:MovieCollectionViewCell.reuseIdentifier,for:indexPath)保护collectionView === collectionData.view else {返回单元格} ...
} func collectionView(_ collectionView:UICollectionView,willDisplay cell:UICollectionViewCell,forItemAt indexPath:IndexPath){保护collectionView === collectionData.view else {return} ...
}

最后一件事,有了该代码,您甚至不需要在保存UICollectionViewUITableViewCell中管理prepareForResue函数,但是您需要做的是将CollectionData对象和数据源连接到UICollectionView中。出队的单元格。

您可以在子控制器中编写如下的便捷函数:

  func registerCollectionView(_ collectionView:UICollectionView){ 
collectionView.delegate =自我
collectionView.dataSource =自我
collectionView.register(MovieCollectionViewCell.self,forCellWithReuseIdentifier:MovieCollectionViewCell.reuseIdentifier)collectionData.validationDelegate =自我
collectionData.view = collectionView
}

然后,当您使UITableViewCell出队时,可以调用此方法。

这种情况最终要复杂得多,但是当您退后一步看时,实际上实际上是为支持相当复杂的功能而编写的代码很少。

方案8:分段轮播表(轮播可以在分段之间移动)

好的,如果我们想从上面理解概念,在表视图行中收集视图并使它们能够像场景6中那样在各节之间移动?

好吧,实际上这只是场景6和场景7的混合。这里唯一的重大变化是采用上一个示例CarouselRow的行数据结构,并将其包装在我们称为CarouselSection的section结构中。 。

这是此更新的数据结构的样子:

  struct CarouselSection:等于{ 
let sectionIdentifier:字符串
让轮播:[CarouselRow]
}扩展名CarouselSection:UniquelyIdentifiableSection {
var uniqueID:字符串{返回sectionIdentifier}
var项目:[CarouselRow] {返回轮播}
var hashValue:Int {return carousels.reduce(uniqueID.hashValue){$ 0 ^ $ 1.hashValue}
}
}

其他所有内容的工作几乎都与您期望的完全相同,并且动画应该可以正常工作(敲木头)。


有关CollectionDataManualReloadDelegate的附加说明

虽然我们在上面使用此委托为我们提供了一个钩子来触发UICollectionView动画,但需要注意的是,我们可以使用此钩子来执行任何动画。

当我们在willHandleReload为indexPath返回true时 ,我们将在reloadItems中再次将其传递给reloadItems 。 随附的完成块indexPathCompletion可使我们执行的所有自定义动画与表/集合视图中的封闭动画保持同步,并确保在完成这些索引路径动画之前,不会开始对该视图进行任何后续更新。

这对我意味着什么?

这意味着您可以用自定义动画替换任何表或集合视图单元格重新加载,并确保与随附的动画一起安全地计时。 只需从视图中获取现有单元格,并公开包含动画完成块的更新功能即可。 这是一个例子:

 让itemCompletion = {indexPathCompletion(indexPath)}守护let cell = collectionView.cellForItem(at:indexPath) 
如? MyCellType else {
itemCompletion()
返回//(或继续)
}让数据= collectionData [indexPath.item]
cell.update(newData,动画:true,完成:itemCompletion)

然后直接在您的单元上执行动画。 这可以将单元格中的短暂闪烁变为良好的,干净的状态转换。


往下

第5部分:数据工厂和高级功能

重访

第1部分:使用iOS的LiveCollections进行动画处理

第2部分:单节视图

第3部分:多部分视图

如果您想改变世界的阅读方式,请加入我们! www.scribd.com/careers

资源资源

从Scribd的GitHub存储库下载