CVGenericDataSource 1.0.1

CVGenericDataSource 1.0.1

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布最后发布2017年4月
SwiftSwift 版本3.0
SPM支持 SPM

Stephan Schulz 维护。



CVGenericDataSource

CVGenericDataSource 是一个用于 UICollectionView 的通用数据源。通过提供类型安全的配置结构来简化使用,并且由于无需再实现 UICollectionViewDataSourceUICollectionViewDelegateUICollectionViewDataSourcePrefetching 协议,所以减少了样板代码。该库具有内置的基于状态、渐进式加载、同步或差异以及更精细的集合视图单元格生命周期功能。

dataSource = CVDataSource(
    sections: [
        CVSection([Entity(), Entity(), Entity()], [
            .selection { (entity, index) in
                // do something
            }
        ]),
    ],
    cellFactory:
        CVCellFactory<Entity, Cell>([
            .setup { (cell, entity, indexPath) in
                // do something
            }
        ]),
    options: [
        .cellSpacing(1)
    ])

dataSource.bind(collectionView: collectionView)

要求

  • iOS 9.0+
  • Swift 3.0+
  • Xcode 8.1+

用法

您可以在本存储库的演示项目部分找到更多信息。

基本示例

为了简洁起见,这里只展示了数据源的小功能集。

class ViewController: UIViewController {

    @IBOutlet weak var collectionView: UICollectionView!

    var viewModel: ViewModel!

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.dataSource.bind(collectionView: collectionView)
    }
}

class ViewCell: CVCell {

    @IBOutlet weak var label: UILabel!

    override class var ReuseIdentifier: String {
        return String(describing: BasicCell.self)
    }
}

struct ViewModel {

    struct Content: Equatable {
    
        var title: String?
    }
    
    let dataSource: 
    CVDataSource<CVSection<Content>,
    CVCellFactory<Content, ViewCell>,
    CVSupplementaryViewFactory<UICollectionReusableView>,
    CVStateFactory<UICollectionViewCell, UICollectionViewCell>>
    
    init() {
        dataSource = CVDataSource(
            sections: [
                CVSection([
                    Content(title: "Hello"),
                    Content(title: "World")])],
            cellFactory:
                CVCellFactory<Content, ViewCell>([
                    .size(CGSize(width: 100, height: 100))
                    .setup { (cell, item, indexPath) in
                        cell.label.text = item?.title
                    }
                ]), 
            options: [
                .selection { (section, item, cell, indexPath) in
                    // do something                
                }
            ]
        )
    }
}

func == (a: ViewModel.Content, b: ViewModel.Content) -> Bool {
    return a.title == b.title
}    

从上到下

  • 数据源的设计原则之一是保持我们的 UIViewController 精简,因此在这个示例中,我们只需要使用 bind(collectionView: UICollectionView) 方法将集合视图绑定到数据源。

  • 所用的集合视图单元格 ViewCellCVCell 的子类,它提供了更精细的生命周期,例如 createlayoutpreparereset 方法。如果我们不想子类化 CVCell,我们只需实现 CVReusableViewProtocol。类变量 ReuseIdentifier 将帮助我们自动从集合视图中排序列表项,而无需手动注册单元格。

  • 我们用于每个集合视图单元格的模型称为 Content。它是一个符合 Equatable 协议的结构体,以便允许同步或差异。

  • 数据源将通过三个参数创建:sectionscellFactoryoptions

    • sections 包含所有要显示在集合视图中的节和单元格的数据结构。
    • cellFactory 包含将生成我们的集合视图单元格的工厂。工厂中给出的类型定义了我们将为每个单元格使用的模型,例如 Content,以及将自动排序列表项的 UICollectionViewCell,例如 ViewCell。通过在工厂的初始化器中使用选项,我们定义了每个集合视图单元格的大小并设置了单元格。
    • options 包含数据源额外的选项。在示例中,我们使用 selection 选项来创建用户选择单元格的回调。

  • 由于我们的实体 Content 符合 Equatable,最后是我们的比较函数。

概述

状态

当使用集合视图时,一个常见的场景是在集合视图为空或开始加载数据时显示某个单元格。为此,数据源提供了一个状态工厂,将自动出列并显示为空或加载状态的单个单元格。数据源可以有以下状态:

public enum CVDataSourceState {
    case inited, empty, ready, loading
}

inited 状态出现在数据源初始化后立即。假设数据源初始化没有分区,默认行为是一旦将集合视图绑定到数据源,就切换到 empty 状态。数据源将按队列顺序出列并显示状态工厂中给出的 UICollectionViewCell。

CVDataSource(
    sections: [],
    ...
    stateFactory: CVStateFactory<EmptyCell, LoadingCell>([
        .setup { (type: CVStateFactoryType, view) in
            // do something
        },
        .size {
            CGSize
        }
    ]),
    options: [
        .shouldShowState { state in ... }
    ]
)

可以使用 .shouldShowState { state in ... } 选项来控制状态单元格的显示。每个时间状态变成 emptyloading 时,都会执行闭包。通过返回 truefalse,我们决定集合视图是否应该显示 emptyloading 单个单元格。

一旦开始加载数据,状态将变为 loading。如果在这个时候数据源是空的且已将集合视图绑定,则集合将显示在状态工厂中配置为 loading 状态的单元格,除在 CVStateFactory 选项中使用 .size 选项外,否则集合视图将保持不变。

一旦完成加载数据,状态将变为 ready 或如果加载数据后数据源仍然是空的,则变为 empty

加载数据操作

状态 结合,数据源支持加载数据操作。这是便利功能的意图,以使数据处理更加容易。例如,分页可能是一个用例。让我们假设我们想在未来的某个点将数据添加到数据源的某个位置。可以使用 load() 方法来触发对应的数据源选项。

.load { (section: Int, offset: Int, result: @escaping CVLoadResult) in {
    // prepare data and return it asynchronously if necessary

result(data, .insert) }

针对数据源中每个现有分区执行 .load 闭包,这允许如果在需要时按每个分区添加更多项目。

public enum CVDataSourceOperation {
    case insert, synchronize
}
  • .insert 将追加新数据
  • .synchronize 将根据旧、新数据的差异删除或添加项目

参数 offset 将包含特定 section 已加载的项目数量。一旦为特定分区调用了 result 闭包,数据源将自动在相应的集合视图中渲染更改。与 渐进式加载概念 一起使用的是 load 函数。

将自动渲染集合视图的其他加载数据操作是:

// Will load a particular section
load(section: Int)

// Will remove all existing sections and call load() afterwards reload()

// Will remove all existing sections, add new sections and call load() afterwards reload(newSections sections: [S])

渐进式加载

数据源支持对垂直和水平集合视图布局进行渐进式加载,这意味着当集合视图的滚动位置接近最大值时,数据源会尝试自动加载新数据。这在分页集合视图的情况下经常会用到。默认情况下,渐进式加载是关闭的。要启用此功能,请使用 .shouldLoadMore { true } 选项。

.shouldLoadMore 选项可能会返回一个动态值,如果没有更多的数据可用,渐进式加载可以关闭。以下示例展示如何在分页集合视图中使用这个选项。

var shouldLoadMore = true

CVDataSource( ... options: [ .load { (section: Int, offset: Int, result: @escaping CVLoadResult) in { let data: [S.Item] = ...

        shouldLoadMore <span class="pl-k">=</span> data.<span class="pl-c1">count</span> <span class="pl-k">&gt;</span> <span class="pl-c1">0</span> <span class="pl-k">&amp;&amp;</span> data.<span class="pl-c1">count</span> <span class="pl-k">&gt;=</span> pageSize

        <span class="pl-c1">result</span>(data, .<span class="pl-smi">insert</span>)
    },
    .<span class="pl-smi">shouldLoadMore</span> { 
        shouldLoadMore
    }
]

)

如果我们想要抑制每当达到集合视图滚动位置的最大值时都调用 load(),我们可以使用 .loadMore 选项进行自定义操作。

CVDataSource(
    ...
    options: [
        .loadMore { 
            // do something different instead of using load() automatically            
        }
    ]
)

同步

数据源中的每个部分都可以使用以下方法进行同步。

func synchronize(sections: [S])
func synchronize(items: [S.Item], inSection section: Int)

这将比较新数据与某个部分已存在的数据,并删除所有不再是新数据部分的项目,并添加所有不再是现有数据部分的新项目。由于泛型类型 S.Item 实现了 Equatable,比较可以通过 func == (a: S.Item, b: S.Item) -> Bool 定义。数据源将自动在对应的集合视图中渲染所有变更。

预取

数据源支持使用相应的选项通过 iOS 10 引入的预取功能。

.prefetch { (section: Int, items: [S.Item]) in

} .cancelPrefetch { (section: Int, items: [S.Item]) in

}

单元格和补充视图

由于集合视图单元格和补充视图是自动注册和出列的,我们需要在实例化数据源时指定它们的类型、reuseIdentifier以及可选的nib名称。这可以通过使用 CVCellFactoryCVStateFactoryCVSupplementaryViewFactory 来完成,以及覆盖以下类变量:

override class var ReuseIdentifier: String {
    return String(describing: ...)
}

override class var NibName: String { return String(describing: ...) }

通过为不同类型的单元格和补充视图使用工厂,我们可以指定它们的类型和数据源中特定项目的关联关系。

class Cell: CVCell {
    override class var ReuseIdentifier: String {
        return String(describing: Cell.self)
    }
}

class SupplementaryView: CVSupplementaryView { override class var ReuseIdentifier: String { return String(describing: SupplementaryView.self) } }

class EmptyCell: CVCell { override class var ReuseIdentifier: String { return String(describing: EmptyCell.self) } }

class LoadingCell: CVSupplementaryView { override class var ReuseIdentifier: String { return String(describing: LoadingCell.self) } }

CVDataSource( sections: [ CVSection([Entity(), Entity(), Entity()]) ], cellFactory: CVCellFactory<Entity, Cell>([ .setup { (cell, entity, indexPath) in // do something }, .size { (entity, indexPath) in CGSize } ]), supplementaryViewFactory: CVSupplementaryViewFactory<SupplementaryView>([ .setup { (type: CVSupplementaryViewType, view, section) in // do something }, .size { CGSize for all views }, .sizeForSection { section in CGSize for a specific section } ]), stateFactory: CVStateFactory<EmptyCell, LoadingCell>([ .setup { (type: CVStateFactoryType, view) in // do something }, .size { (type: CVStateFactoryType) in CGSize } ]) )

要使用数据源中不同类型的单元格或补充视图,请参阅自定义视图示例

单元格和补充视图的生命周期

使用 CVGenericDataSource 时,每个 UICollectionViewCellUICollectionReusableView 都有新的生命周期方法。

// Called when awakeFromNib was called
func create() {
}

// Called when willDisplay was called func prepare() { }

// Called when didEndDisplaying was called func cleanup() { }

通过为集合视图单元格继承 CVCell 或为补充视图继承 CVSupplementaryView,可以提供更多生命周期方法。

// Called when the cell has a valid frame
func layout() {
}

// Called when prepareForReuse was called func reset() { }

布局选项

可以使用以下选项配置布局间距。

CVDataSource(
    sections: [
        CVSection(..., [
            .insets(UIEdgeInsets),
            .lineSpacing(CGFloat),
            .cellSpacing(CGFloat)
        ])
    ],
    ...
    options: [
        .insetsForSection { section in
            UIEdgeInsets
        },
        .insets(UIEdgeInsets),
        .lineSpacing(CGFloat),
        .cellSpacing(CGFloat)
    ]
)

由于可以针对特定部分使用外观选项以及数据源的外观选项,因此将优先使用 CVSection 的选项。如果没有外观选项,则使用数据源的选项。间距的默认值是 0,对称为 .zero

附录

当前版本的 CVGenericDataSource 设计受到 Jesse Squires 的 JSQDataSourcesKit 的极大启发。实际上,我们借鉴了他的实现部分,因此感谢 Jesse!

讨论

  • 虽然 CVGenericDataSource 旨在完全取代 UICollectionViewDataSourceUICollectionViewDelegateUICollectionViewDataSourcePrefetching 协议,但在某些情况下它可能无法正常工作,因为这些协议的全部功能尚未实现。

  • 每个视图类型(单元格和辅助视图)使用一个工厂的概念,在需要使用多个不同的单元格和视图于我们的集合视图中时,并不那么适用。尽管在 自定义视图示例 中已经展示可以实现,但存在大量的样板代码。我喜欢 Matthias 在其 UITableView 数据源实现 中使用每个视图类型的描述符数组的做法。因此,这项改变可能会很快发生。

贡献

欢迎贡献力量,请查看 贡献指南

其他优秀的数据源

许可协议

CVGenericDataSource 采用 MIT 许可协议发布。有关详细信息,请参阅 LICENSE 文件。