LiveCollections 是一个开源框架,它可以使在几行代码中使用 UITableView 和 UICollectionView 动画成为可能。给定两组数据,该框架将自动执行所有计算,构建行项动画代码,并在视图中执行它。
使用这两个主要类之一 CollectionData
或 CollectionSectionData
,您可以构建一个完全通用的不可变数据集,它是线程安全的、时间安全的,并且性能极高。只需将视图连接到数据对象,调用更新方法即可。
在示例应用中,演示了多种用例场景,可以通过查找相应的控制器找到每个用例的示例代码(例如 ScenarioThreeViewController.swift)。
每个场景的详细使用案例可以在 Medium 上的博客文章中找到,如果您想了解完整的解释,可以阅读该文章。下面,我将仅展示类图和每个情况所需的最低代码。
使用 Carthage 导入
github "scribd/LiveCollections" "beta_0.9.8"
使用 CocoaPods 导入
pod 'LiveCollections', '~> 0.9.8'
或
pod 'LiveCollections'
主要类
- CollectionData<您的类型>
- CollectionSectionData<您的分区类型>
通过将数据封装在这两个类之一中,您可以计算更新与当前数据集之间的增量。如果您将视图分配给任何一个对象,它将自动将增量和数据更新传递给该视图并执行动画。所有更新都会排队,性能良好,线程安全,且时序安全。在您的 UITableViewDataSource 和 UICollectionViewDataSource 方法中,您只需直接使用提供的 CollectionData 或 CollectionSectionData 对象及相关 count
、subscript
和 isEmpty
线程安全访问器来获取数据。
为了使数据能够用于 CollectionData,您只需要采用 UniquelyIdentifiable 协议。关于 CollectionSectionData 的具体要求将在稍后的第 5 场景和第 6 场景中详细介绍。
采用 UniquelyIdentifiable 协议
能够将 CollectionData 作为数据源使用,并获取 LiveCollections 的所有好处,关键在于采用 UniquelyIdentifiable 协议。这是框架中私有增量计算器确定您的数据对象所有位置变化的原因。
public protocol UniquelyIdentifiable: Equatable {
associatedtype RawType
associatedtype UniqueIDType: Hashable
var rawData: RawType { get }
var uniqueID: UniqueIDType { get }
}
由于 UniquelyIdentifiable 继承自 Equatable,因此您的基类采用 Equatable 会自动生成等价性函数(如果需要,您还可以编写自定义的 == 函数)。
以下是一个简单示例,说明它如何应用于自定义数据类:
import LiveCollections
struct Movie: Equatable {
let id: UInt
let title: String
}
extension Movie: UniquelyIdentifiable {
typealias RawType = Movie
var uniqueID: UInt { return id }
}
注意:请查看以下第 9 个场景,以查看一个示例,其中 RawType 不简单是 Self 类型。如果我们在不同的视图中为相同的 RawType 对象提供不同的等价性函数,或者如果我们想要创建一个包含附加元数据的新对象,我们将使用不同的类型。
采用 NonUniquelyIdentifiable 协议
还增加了对非唯一数据集的支持。
public protocol NonUniquelyIdentifiable: Equatable {
associatedtype NonUniqueIDType: Hashable
var nonUniqueID: NonUniqueIDType { get }
}
通过采用此协议并使用两个类型别名之一(即 NonUniqueCollectionData
或 NonUniqueCollectionSectionData
),将自动构建一个工厂,该工厂将您的非唯一数据转换为 UniquelyIdentifiable
类型。请参阅场景 10 和 11。
由于数据被包裹在一个新的结构体中,要访问您的原始对象,您将需要调用 rawData
属性访问器,如下所示
let data = collectionData[indexPath.item].rawData
注意:这将使用“最佳猜测”逻辑,标识符将根据数组顺序确定。
以下是您的应用程序中每个场景所需的相关代码汇总。这些反映了您在管理时所需的大多数用例。
场景 1:一个具有一个分区的UICollectionView
final class YourClass {
private let collectionView: UICollectionView
private let collectionData: CollectionData<YourData>
init(_ data: [YourData]) {
collectionData = CollectionData(data)
...
super.init()
collectionData.view = collectionView
}
func someMethodToUpdateYourData(_ data: [YourData]) {
collectionData.update(data)
}
}
extension YourClass: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return collectionData.count
}
// etc
}
场景 2:一个具有一个分区的UITableView
与场景 1 相同,但将 MatTable 替换为 UITableView。
场景 3:具有多个分区的UICollectionView,每个分区都有其自己的CollectionData
或
final class YourClass {
private let collectionView: UICollectionView
private let dataList: [CollectionData<YourData>]
init() {
// you can also assign section later on if that better fits your class design
dataList = [
CollectionData<YourData>(section: 0),
CollectionData<YourData>(section: 1),
CollectionData<YourData>(section: 2)
]
...
super.init()
// Optionally apply a synchronizer to multiple sections to have them
// perform their animations in the same block when possible
let synchronizer = CollectionDataSynchronizer(delay: .short)
dataList.forEach { $0.synchronizer = synchronizer }
}
func someMethodToUpdateYourData(_ data: [YourData], section: Int) {
let collectionData = dataList[section]
collectionData.update(data)
}
}
extension YourClass: UICollectionViewDelegate {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return dataList.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let collectionData = dataList[section]
return collectionData.count
}
// let item = collectionData[indexPath.row]
// etc
}
场景 4:具有多个分区的UITableView,每个分区都有其自己的CollectionData
与场景 3 相同,但将 UICollectionView 替换为 UITableView。
在使用多个分区的独特数据时采用 UniquelyIdentifiableSection 协议
当数据项在整个视图中唯一表示时,它们可能会在分区之间移动。为了处理这些动画,您可以使用 CollectionSectionData 并创建一个采用 UniquelyIdentifiableSection 的数据项。
public protocol UniquelyIdentifiableSection: UniquelyIdentifiable {
associatedtype DataType: UniquelyIdentifiable
var items: [DataType] { get }
}
如你所见,这最终仍然依赖于采用 UniquelyIdentifiable 的基本数据类型。这个新对象帮助我们包装分区更改。
注意:由于 UniquelyIdentifiableSection 继承自 UniquelyIdentifiable,这意味着每个分区也需要它的唯一ID来跟踪分区更改。这些ID不必与底层 items: [DataType]
中的ID相同。
import LiveCollections
struct MovieSection: Equatable {
let sectionIdentifier: String
let movies: [Movie]
}
extension MovieSection: UniquelyIdentifiableSection {
var uniqueID: String { return sectionIdentifier }
var items: [Movie] { return movies }
var hashValue: Int { return items.reduce(uniqueID.hashValue) { $0 ^ $1.hashValue } }
}
场景 5:一个具有多个分区和单个数据源的 UICollectionView
final class YourClass {
private let collectionView: UICollectionView
private let collectionData: CollectionSectionData<YourSectionData>
init(_ data: [YourSectionData]) {
collectionData = CollectionSectionData(data)
...
super.init()
collectionData.view = collectionView
}
func someMethodToUpdateYourData(_ data: [YourSectionData]) {
collectionData.update(data)
}
}
extension YourClass: UICollectionViewDelegate {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return collectionData.sectionCount
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return collectionData.rowCount(forSection: section)
}
// let item = collectionData[indexPath]
// etc
}
场景 6:一个具有多个分区和单个数据源的 UITableView
与场景 5 相同,但将 UICollectionView 替换为 UITableView(你可能没猜到这一点)。
场景 7:一个carousel表格
表格视图数据源
final class YourClass {
private let tableView: UITableView
private let collectionData: CollectionData<YourData>
private let carouselDataSources: [SomeCarouselDataSource]
init(_ data: [YourData]) {
collectionData = CollectionData(data)
...
super.init()
collectionData.view = tableView
}
func someMethodToUpdateYourData(_ data: [YourData]) {
collectionData.update(data)
}
// some function that fetches a carousel data source based on identifier
private func _carouselDataSource(for identifier: Int) -> SomeCarouselDataSource {
....
}
}
extension YourClass: UITableViewDelegate {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// hook up SomeCarouselDataSource to the cell containing the collection view
// ideally wrap this logic into a private func rather than exposing these variables
...
let carouselRow = collectionData[indexPath.row]
let carouselDataSource = _carouselDataSource(for: carouselRow.identifier)
cell.collectionView.dataSource = carouselDataSource
cell.collectionView.delegate = carouselDataSource
carouselDataSource.collectionData.view = cell.collectionView
...
}
// etc
}
extension YourClass: CollectionDataManualReloadDelegate {
// IndexPathPair is a struct that contains both the source index path for the original data set
// and the target index path of the updated data set. You may need to know one or both pieces
// of information to determine if you want to handle the reload.
func willHandleReload(at indexPathPair: IndexPathPair) -> Bool {
return true
}
func reloadItems(at indexPaths: [IndexPath], indexPathCompletion: @escaping (IndexPath) -> Void) {
indexPaths.forEach { indexPath in
let carouselRow = collectionData[indexPath.item]
let carouselDataSource = _carouselDataSource(for: carouselRow.identifier)
let itemCompletion = {
indexPathCompletion(indexPath)
}
carouselDataSource.update(with: carouselRow.movies, completion: itemCompletion)
}
}
func preferredRowAnimationStyle(for rowDelta: IndexDelta) -> AnimationStyle {
return .preciseAnimation
}
}
一个表格视图单元格来包含收集视图
final class CarouselTableViewCell: UITableViewCell {
private let collectionView: UICollectionView
...
}
carousel数据源
final class SomeCarouselDataSource: UICollectionViewDelegate {
private let collectionView: UICollectionView
private let collectionData: CollectionData<YourData>
func someMethodToUpdateYourData(_ data: [YourData], completion: (() -> Void)?) {
collectionData.update(data, completion: completion)
}
}
extension SomeCarouselDataSource: CollectionDataReusableViewVerificationDelegate {
func isDataSourceValid(for view: DeltaUpdatableView) -> Bool {
guard let collectionView = view as? UICollectionView,
collectionView.delegate === self,
collectionView.dataSource === self else {
return false
}
return true
}
}
extension SomeCarouselDataSource: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SomeCarouselCell.reuseIdentifier, for: indexPath)
// NOTE: Very imporatant guard. Any time you need to fetch data from an index, first guard that it
// is the correct collection view. Dequeueing table view cells means that we can get into
// situations where views temporarily point to outdated data sources.
guard collectionView === collectionData.view else {
return cell
}
let item = collectionData[indexPath.item]
...
}
}
场景 8:具有跨越分区的carousel表格(carousel可以在分区之间移动)
几乎与上一个例子相同,只是我们的表格视图使用 CollectionSectionData。
final class YourClass {
private let tableView: UITableView
private let collectionData: CollectionSectionData<YourSectionData>
private let carouselDataSources: [SomeCarouselDataSource]
...
}
场景 9:使用数据工厂
您创建的数据工厂必须符合 UniquelyIdentifiableDataFactory 协议。它的作用是简单地接受一个 RawType
值并构建一个新的对象。这可以是一个简单的包装类,它修改等价函数,或者可以是一个复杂的工厂,它注入多个数据服务以获取 RawType 项的元数据。
使用数据工厂意味着您的 CollectionData 上的更新方法接受 [RawType],但在通过 subscript
请求值时返回 [UniquelyIdentifiableType](您的新的数据类)值。这也使您的数据源无需了解任何您构建的自定义类型,即可自定义视图。
buildQueue
属性将默认为 nil,通过扩展实现,仅在有特定线程上构建数据的需要时使用。如果没有这个需求,请忽略它。
public protocol UniquelyIdentifiableDataFactory {
associatedtype RawType
associatedtype UniquelyIdentifiableType: UniquelyIdentifiable
var buildQueue: DispatchQueue? { get } // optional queue if your data is thread sensitive
func buildUniquelyIdentifiableDatum(_ rawType: RawType) -> UniquelyIdentifiableType
}
以下是在示例应用中使用的示例。它接受一个注入的控制台,查找电影当前是否在影院上映,并创建一个包含这些数据的新しい对象。等价函数包括此元数据,因此改变了视图动画中构成 reload
动作的条件。
import LiveCollections
struct DistributedMovie: Hashable {
let movie: Movie
let isInTheaters: Bool
}
extension DistributedMovie: UniquelyIdentifiable {
var rawData: Movie { return movie }
var uniqueID: UInt { return movie.uniqueID }
}
struct DistributedMovieFactory: UniquelyIdentifiableDataFactory {
private let inTheatersController: InTheatersStateInterface
init(inTheatersController: InTheatersStateInterface) {
self.inTheatersController = inTheatersController
}
func buildUniquelyIdentifiableDatum(_ movie: Movie) -> DistributedMovie {
let isInTheaters = inTheatersController.isMovieInTheaters(movie)
return DistributedMovie(movie: movie, isInTheaters: isInTheaters)
}
}
构建您的工厂后,您代码中的主要变化是将其注入到初始化器中。
final class YourClass {
private let collectionView: UICollectionView
private let collectionData: CollectionData<Movie>
init(_ movies: [Movie]) {
let factory = DistributedMovieFactory(inTheatersController: InTheatersController())
collectionData = CollectionData(dataFactory: factory, rawData: movies)
...
super.init()
collectionData.view = collectionView
}
func someMethodToUpdateYourData(_ movies: [Movie]) {
collectionData.update(movies)
}
}
extension YourClass: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return collectionData.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
...
// note the different type being returned
let distributedMovie = collectionData[indexPath.item]
...
}
// etc
}
场景 10:单个部分中的非唯一数据
使用类型别名的数据结构 NonUniqueCollectionData
与您的非唯一数据进行配合。
final class YourClass {
private let collectionView: UICollectionView
private let collectionData: NonUniqueCollectionData<YourData>
init(_ data: [YourData]) {
collectionData = CollectionData(data)
...
super.init()
collectionData.view = collectionView
}
func someMethodToUpdateYourData(_ data: [YourData]) {
collectionData.update(data)
}
}
extension YourClass: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return collectionData.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
...
// note that your data is wrapped in a unique type, it must be fetched with rawData
let movie = collectionData[indexPath.item].rawData
...
}
// etc
}
场景 11:多个部分中的非唯一数据
使用类型别名的数据结构 NonUniqueCollectionSectionData
与您的非唯一部分数据一起使用。
final class YourClass {
private let collectionView: UICollectionView
private let collectionData: NonUniqueCollectionSectionData<YourSectionData>
init(_ data: [YourSectionData]) {
collectionData = CollectionData(view: view, sectionData: data)
...
super.init()
}
func someMethodToUpdateYourData(_ data: [YourData]) {
collectionData.update(data)
}
}
extension YourClass: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
...
// note that your data is wrapped in a unique type, it must be fetched with rawData
let movie = collectionData[indexPath.item].rawData
...
}
// etc
}
场景 12:手动设置动画时间
在前面的每个场景中,我们都已将视图对象分配给 CollectionData
对象。如果您选择省略此步骤,您仍然可以获取 LiveCollections 计算的好处。
只需做以下操作
let delta = collectionData.calculateDelta(data)
// perform any analysis or analytics on the delta
let updateData = {
self.collectionData.update(data)
}
// when the time is right, call...
collectionView.performAnimations(section: collectionData.section, delta: delta, updateData: updateData)
注意:此功能对 CollectionSectionData 不可用,因为动画发生在多个步骤中,更新的时间非常特定。
场景 13:自定义表格视图动画
LiveCollections 默认执行一系列预设的表格视图动画:删除 (.bottom),插入 (.fade),重新加载 (.fade),重新加载部分 (.none)。
从 0.9.8 版本开始,支持覆盖这些默认值并设置自己的值。
在 CollectionData
上有一个新的访问器,可以设置您的视图而不是使用 collectionData.view = tableView
。
let rowAnimations = TableViewAnimationModel(deleteAnimation: .right,
insertAnimation: .right,
reloadAnimation: .middle)
collectionData.setTableView(tableView,
rowAnimations: rowAnimations,
sectionReloadAnimation: .top)
场景 14:针对同一表格视图的多个数据源具有自定义动画
如果您有多个数据源,每个数据源都在动画化表格视图的一部分,您可以给每个数据源自定义动画。如果确实需要,它们甚至可以按部分不同。
let sectionZeroRowAnimations = TableViewAnimationModel(deleteAnimation: .left,
insertAnimation: .left,
reloadAnimation: .left)
dataList[0].setTableView(tableView, rowAnimations: sectionZeroRowAnimations)
let sectionOneRowAnimations = TableViewAnimationModel(deleteAnimation: .right,
insertAnimation: .right,
reloadAnimation: .right)
dataList[1].setTableView(tableView, rowAnimations: sectionOneRowAnimations)
let sectionTwoRowAnimations = TableViewAnimationModel(deleteAnimation: .top,
insertAnimation: .top,
reloadAnimation: .top)
dataList[2].setTableView(tableView, rowAnimations: sectionTwoRowAnimations)
场景 15:适用于所有部分的自定义表格视图动画
CollectionSectionData
有一个新的初始化器。
let rowAnimations = TableViewAnimationModel(deleteAnimation: .right,
insertAnimation: .right,
reloadAnimation: .right)
let sectionAnimations = TableViewAnimationModel(deleteAnimation: .left,
insertAnimation: .left,
reloadAnimation: .left)
return CollectionSectionData<MovieSection>(tableView: tableView,
sectionData: initialData,
rowAnimations: rowAnimations,
sectionAnimations: sectionAnimations)
我希望这几乎涵盖了所有用例,但如果您发现此框架提供的功能存在差距,我非常愿意听取您的建议和反馈。
祝您动画愉快!
特别感谢 电影数据库。示例应用程序中的所有图像和数据都是从其开源 API 中检索的。这是一个出色的工具,帮助节省了很多时间和麻烦。使用他们的数据,这些示例演示了如何使用 LiveCollections 扩展现有数据。
本产品使用 TMDb API,但并非由 TMDb 推荐或认证。