LiveCollections 0.9.8

LiveCollections 0.9.8

Stephane MagneStephane Magne 维护。



  • Stephane Magne

LiveCollections logo

Single section collection view class graph

LiveCollections 是一个开源框架,它可以使在几行代码中使用 UITableView 和 UICollectionView 动画成为可能。给定两组数据,该框架将自动执行所有计算,构建行项动画代码,并在视图中执行它。

使用这两个主要类之一 CollectionDataCollectionSectionData,您可以构建一个完全通用的不可变数据集,它是线程安全的、时间安全的,并且性能极高。只需将视图连接到数据对象,调用更新方法即可。

在示例应用中,演示了多种用例场景,可以通过查找相应的控制器找到每个用例的示例代码(例如 ScenarioThreeViewController.swift)。

每个场景的详细使用案例可以在 Medium 上的博客文章中找到,如果您想了解完整的解释,可以阅读该文章。下面,我将仅展示类图和每个情况所需的最低代码。


使用 Carthage 导入


github "scribd/LiveCollections" "beta_0.9.8"


使用 CocoaPods 导入


pod 'LiveCollections', '~> 0.9.8'

pod 'LiveCollections'


主要类

  • CollectionData<您的类型>
  • CollectionSectionData<您的分区类型>

通过将数据封装在这两个类之一中,您可以计算更新与当前数据集之间的增量。如果您将视图分配给任何一个对象,它将自动将增量和数据更新传递给该视图并执行动画。所有更新都会排队,性能良好,线程安全,且时序安全。在您的 UITableViewDataSource 和 UICollectionViewDataSource 方法中,您只需直接使用提供的 CollectionDataCollectionSectionData 对象及相关 countsubscriptisEmpty 线程安全访问器来获取数据。

为了使数据能够用于 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 }
}

通过采用此协议并使用两个类型别名之一(即 NonUniqueCollectionDataNonUniqueCollectionSectionData),将自动构建一个工厂,该工厂将您的非唯一数据转换为 UniquelyIdentifiable 类型。请参阅场景 10 和 11。

由于数据被包裹在一个新的结构体中,要访问您的原始对象,您将需要调用 rawData 属性访问器,如下所示

let data = collectionData[indexPath.item].rawData

注意:这将使用“最佳猜测”逻辑,标识符将根据数组顺序确定。


以下是您的应用程序中每个场景所需的相关代码汇总。这些反映了您在管理时所需的大多数用例。

场景 1:一个具有一个分区的UICollectionView

Single section collection view class graph

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

Multiple discrete sections collection view class graph

Multiple discrete sections collection view class graph with two sections synchronized

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

Multiple section collection view class graph

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表格

Table of carousels class graph

表格视图数据源

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可以在分区之间移动)

Sectioned table of carousels class graph

几乎与上一个例子相同,只是我们的表格视图使用 CollectionSectionData。

final class YourClass {
    private let tableView: UITableView
    private let collectionData: CollectionSectionData<YourSectionData>
    private let carouselDataSources: [SomeCarouselDataSource]
    
    ...
}



场景 9:使用数据工厂

Using a data factory class graph

您创建的数据工厂必须符合 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 推荐或认证。