LiveCollections第5部分:数据工厂,非唯一数据和高级功能

最后要看的重要功能是数据工厂,稍后我将对其进行解释,然后我将介绍一些我尚未介绍的其他功能(因为我只是不够擅长于其他功能)将它们放入前面的示例中)。

数据工厂:这是什么?为什么要使事情变得更复杂?

当我开始这个项目时,这是我没有想到的功能,但是在Scribd中用LiveCollections制作食物后,这项功能成为必需的功能。

必需的是, CollectionData中的计算基于采用UniquelyIdentifiable通用类型,因此,使用默认的对象相等性函数定义相等性比较。 这是有问题的,因为我们必须对每种数据类型的均等性进行单一定义。

尽管这对于数据管理是正常的,但对于该数据的视图表示而言却不是理想的选择。 平等本质上是定义视图中行/项目的重载 。 这种不平等告诉我们发生了变化。 问题是我们可能不想在数据的两个不同表示形式中使用相同的相等概念 。 具体来说,我们可能需要考虑其他信息,这些信息会修改我们的数据,但它们是扩展的元数据,而不是根属性。

有两种方法可以解决此问题:

  1. 只需创建一个包装类/结构来定义数据类型和其他元数据的元组。
  2. 创建相同的包装类/结构,但使用采用UniquelyIdentifiableDataFactory协议的对象来支持它,并将为您构建包装器。

解决方案1在有限的情况下完全适用,而解决方案2隐藏了映射逻辑以转换数据,并允许您公开标准化的更新方法(您可能需要这种方法来实现自定义协议一致性,注入和可测试性)。

如果包装类型是标准类型Movie MovieWrapper ,则这是方案1的更新函数:

  func update(_数据:[ MovieWrapper ],完成:(()-> Void)?) 

对于解决方案2:

  func update(_数据:[ Movie ],完成:(()->无效)?) 

用法和抽象方面的微小但重要的变化,因为它与到目前为止我们在所有示例中一直使用的相同API相匹配。

好的,我们如何建立数据工厂?

我们将在最终方案中对此进行回答…

方案9:使用数据工厂

让我们从一个简短而简单的情况开始。 数据对象将是Movie ,而我们要添加的元数据是boolean isInTheaters: Bool 。 本质上,即使电影结构上的所有数据保持完全相同,但是电影结束了其戏剧性的放映,我们仍要在视图中重新加载该单元格(大概是删除横幅或徽章)。

为此,我们需要一个将在CollectionData使用的新结构。 我们可以将其称为DistributedMove以表示已拾取要在剧院上映的电影。 看起来像这样:

  struct DistributedMovie:哈希{ 
让电影:电影
let isInTheaters:布尔
}
扩展名DistributedMovie:UniquelyIdentifiable {
var rawData:电影{返回电影}
var uniqueID:UInt {return movie.uniqueID}
}

这是一个非常简化的模型,但是适合我们的需求。

在此示例中,我们还将具有一些控制器,该控制器可以向我们提供信息以确定电影当前是否在影院中。 让我们将此对象InTheatersController ,它将公开单数函数func isMovieInTheaters(_ movie: Movie) -> Bool

好的,我们现在拥有建立工厂所需的所有工具。 这个新的工厂类将需要采用协议UniquelyIdentifiableDataFactory

 公共协议UniquelyIdentifiableDataFactory { 
关联类型RawType
relatedtype UniquelyIdentifiableType:UniquelyIdentifiable
var buildQueue:DispatchQueue? {得到}
func buildUniquelyIdentifiableDatum(_ rawType:RawType)->
UniquelyIdentifiableType
}

RawType值(您以前会记得始终与Self相同)现在变为Movie 。 这就是确定update方法在CollectionData采用哪种类型的原因。

注意: buildQueue 默认通过扩展名 默认为 nil ,仅在需要在特定队列上构建数据时才需要使用。

该协议并不像实际使用时那样令人恐惧:

  struct DistributedMovieFactory:UniquelyIdentifiableDataFactory {私人let inTheatersController:InTheatersController init(inTheatersController:InTheatersController){ 
inTheatersController = inTheatersController
}
func buildUniquelyIdentifiableDatum(_ movie:Movie)-> DistributedMovie {
let inTheaters = inTheatersState.isMovieInTheaters(电影)
return DistributedMovie(电影:电影,isInTheaters:inTheaters)
}
}

这实际上只是一个持有状态查询控制器并将其应用于数据以构建新对象的对象。

我们如何使用它?

从这里开始,唯一真正更改的代码是初始化程序:

  let dataFactory = DistributedMovieFactory(inTheatersController:inTheatersController)collectionData = CollectionData (dataFactory:dataFactory) 

现在,无论何时调用collectionData.update(movies) ,它都会在后台使用数据工厂来构建新的DistributedMovie对象数组并将其用于相等性。

还要注意,当您使用[Movie]更新时, subscript函数将返回DistributedMovie ,您需要用它来正确装饰单元格。

仅需使用这几个工具,我们现在就可以构建任何灵活的自定义类/结构,从而为我们所需的任何视图提供可自定义的上下文动画!

方案10:单个部分中的非唯一数据

最初的实现仅依赖于具有完全唯一标识符的所有数据,但是我意识到这是不必要的限制因素。 归根结底,所有数据确实都需要唯一表示,但是可以肯定,这可以在后台自动处理,对吗? 对。

解决方案是利用数据工厂以静默方式创建数据对象的唯一表示形式,以管理增量计算。

工厂是在初始化程序中为您创建的,因此您所要做的就是通过采用(可预测名称)协议NonUniquelyIdentifiable来设置数据类,该协议定义为:

 公用协议NonUniquelyIdentifiable:等于{ 
relatedtype NonUniqueIDType:可哈希
var nonUniqueID:NonUniqueIDType {get}
}

然后,不使用CollectionData ,而是使用typealias:

 类型别名 
NonUniqueCollectionData = CollectionData <NonUniqueDatum >

像这样设置数据:

 让collectionData = NonUniqueCollectionData () 

如您所见,它将数据类型包装在容器NonUniqueDatum 。 然后,它将数据转换为容器类型并添加一个occurrence: Int值以帮助使其唯一。

结果,当您通过subscriptitems获取数据时您将需要使用rawData访问器对其进行rawData

 让电影= collectionData [indexPath.item] .rawData 

注意:匹配算法非常基础,如果您的数组是[A,B,A,C,D,B],那么它将被映射到[A(1),B(1),A(2),C,D ,B(2)]并相应执行动画。

方案11:多个部分中的非唯一数据

完全如上所述,只不过您将使用类型别名的集合类型:

 类型别名 
NonUniqueCollectionSectionData
= CollectionSectionData <NonUniqueSectionDatum >
其中NonUniqueSection.DataType:NonUniquelyIdentifiable

并设置您的数据:

 让collectionData = NonUniqueCollectionSectionData (_) 

同样,您的数据也包装在容器对象NonUniqueSectionDatum ,其中您的items也包装为NonUniqueDatum 。 因此,您只需像这样访问数据:

 让电影= collectionData [indexPath] .rawData 

方案12:未在CollectionData上设置视图对象

到目前为止,在所有场景中,我们始终在CollectionData对象上设置一个视图,并使其直接为我们管理动画。 如果我们不这样做怎么办?

最后还有另外一个场景, 场景10 ,它涵盖了这个用例。 基本上,您只是将增量的计算与动画的执行分离。

代码如下:

 让delta = collectionData.calculateDelta(data) 
让updateData = {
self.collectionData.update(数据)
}
collectionView.performAnimations(section:collectionData.section,
三角洲:三角洲,
updateData:updateData,)

或者,如果您决定完全跳过此动画:

  collectionData.update(数据) 
collectionView.reloadData()

如果您不想在每次更新时都使用动画,或者想要更紧密地管理动画的时间,或者您只是想要增量并且完全没有视图,那么这是为了增加灵活性。

由于增量计时的复杂性,我没有在CollectionSectionData上添加等效功能,并且要使用该对象,您仍然必须为其分配视图。


抓斗袋

好吧,让我们总结一下,然后再不小心结束第6部分……这太多了。

剩下的一些功能并没有真正涉及到,但是可以单独解释,让我们在这里进行介绍。

public var dataCountAnimationThreshold:Int

CollectionData具有可设置的变量dataCountAnimationThreshold ,其默认值为10,000。

它的作用是使任何复杂的增量动画短路,并用reloadSection动画替换它们。 我在精神上认为这是在一副纸牌周围涂抹而不是将它们整齐地洗净。 动画并不精确,只是使它们看起来像东西在四处移动,然后显示最终数据集。 它介于reloadDataperformAnimations之间。

如果您还记得第1部分中的性能图表,我们将10,000行确定为实际性能下降点,这就是为什么它是我们此处的默认值。 您可以根据需要提高或降低此数字。

其他CollectionDataManualReloadDelegate函数

实际上,还有一个附加功能:

  func preferredRowAnimationStyle(for rowDelta:IndexDelta)-> AnimationStyle 

其中AnimationStyle是:

 公共枚举AnimationStyle { 
case reloadData //没有动画
case reloadSections //动画但不精确
case precisionAnimations //动画精确
}

除了上面的dataCountAnimationThreshold之外,这还使您可以进行粒度控制来确定要如何对任何特定增量进行动画处理。 某些情况下可能会导致您不想执行的动画,在这里您可以将其“降级”为reloadSections或使用reloadData其完全中止。

在大多数情况下,只需返回.preciseAnimations

委托CollectionSectionDataManualReloadDelegate

我之前没有特别指出,但这是使用CollectionSectionData时必须采用的委托协议。 它继承自CollectionDataManualReloadDelegate并添加了一个方法:

  func preferredSectionAnimationStyle(for sectionDelta:IndexDelta)-> AnimationStyle 

像上面的行动画功能一样,这使您可以确定节的动画。

为了更加清楚起见,部分动画总是在行动画之前出现(即,在从该部分插入,删除,移入/移出或重新加载任何项目之前,移动的部分将以动画形式显示到其新位置)。

委托CollectionDataDeletionNotificationDelegate

您可以采用此协议并将其设置在CollectionDataCollectionSectionData

它具有一个功能:

 公共协议CollectionDataDeletionNotificationDelegate:AnyObject { 
relatedtype数据类型:UniquelyIdentifiable
func didDeleteItems(_项目:[DataType])
}

因为它需要一个associatedtype ,所以设置委托需要您调用:

  collecionData.setDeletionNotificationDelegate(自己) 

通过采用此方法,您将获得在先前更新中删除的项目的列表。 如果您要添加日志记录,发送分析,清除相关对象的内存或仅调试某些代码,这是一个挂钩。

追加数据

我没有在主要示例中介绍这个问题,因为这是一个微不足道的用例。 如果将表或集合视图创建为分页服务,其中所有更新始终都附加在后面,则可以简化计算时间,只需调用:

  collectionData.append(数据) 

因为增量实际上是整数范围,所以追加将忽略dataCountAnimationThreshold并始终尝试进行动画处理。

CollectionSectionData也支持追加,但是在这种情况下,它仅支持追加整个节。 也就是说,您不能将行数据附加到已显示的部分。 您必须调用update才能获得该行为。

查看当前正在计算的内容

使用CollectionData ,使用访问者subscriptitems将使您看到当前数据集,但是由于处理是在后台队列中进行的,因此在一段时间内我们将处理更新的数据集,而以前的数据仍将从中返回。以上访问器。

要查看您是否处于此状态,还有两个附加访问器, isCalculatingisCalculating ,它们使您可以对当前正在处理的数据进行读取访问。 您可以选择忽略这些信息,但是对于基于您的工作流的某些逻辑决策可能会有所帮助。

同样,对于CollectionSectionData ,有等效的方法isCalculatingisCalculating


结束

大功告成!

多亏了所有致力于此的读者,我真诚地希望您能发现LiveCollections是一种工具,既可以为您的应用程序增添视觉效果,又可以减轻使用整站动画的压力。

如果您对新功能或代码调整有任何建议或反馈,我希望收到您的来信。 随时在GitHub上提交问题,或直接通过stephane@scribd.com向我发送电子邮件。

我还要感谢我在Scribd的所有开发人员,无论过去还是现在,Ibrahim Sha’ath,Paris Pinkney和ThéophaneRupin在错误修复,代码审查或设计反馈方面所做的贡献。


重访

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

第2部分:单节视图

第3部分:多部分视图

第四部分:轮播表

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

资源资源

从Scribd的GitHub存储库下载