DataSourceKit
UICollectionView 和 UITableView 的声明式、可测试的数据源。
安装
有两种方法将 DataSourceKit 安装到您的项目中。
CocoaPods
在 Podfile 中添加一行 pod "DataSourceKit"
,然后运行 pod install
。
Carthage
在 Cartfile 中添加一行 github "ishkawa/DataSourceKit"
,然后运行 carthage update
。
简单用法
- 让单元格遵守 BindableCell。
- 让视图控制器遵守 CellsDeclarator。
- 创建一个 CollectionViewDataSource 的实例,并将其分配给 UICollectionView 的 dataSource。
让单元格遵从 BindableCell
为了使单元格在 DataSourceKit 机制中可用,让单元格遵从 BindableCell。BindableCell 是一个协议,它提供注册单元格到 UICollectionView 和绑定单元格与值界面的接口。
例如,下面的实现表明 ReviewCell 将通过名为 "ReviewCell" 的 UINib 和 "Review" 的重用标识符进行注册,并且单元格将与 Review 绑定。
extension ReviewCell: BindableCell {
static func makeBinder(value review: Review) -> CellBinder {
return CellBinder(
cellType: ReviewCell.self,
nib: UINib(nibName: "ReviewCell", bundle: nil),
reuseIdentifier: "Review",
configureCell: { cell in
cell.authorImageView.image = review.authorImage
cell.authorNameLabel.text = review.authorName
cell.bodyLabel.text = review.body
})
}
}
让视图控制器遵从 CellsDeclarator
下一步是声明单元格的排列。CellsDeclarator 是一个用于此目的的协议。
假设在一个视图控制器中有以下数据
final class VenueDetailViewController: UIViewController {
var venue: Venue
var reviews: [Review]
var relatedVenues: [Venue]
}
要声明单元格的排列,将单元格的 makeBinder(value:)
通过 declareCells(_:)
中的 cell(_:)
传递给 declareCells(_:)
。由于对 cell(_:)
的调用将被转换为实际的单元格,因此请按照您想要显示单元格的顺序调用 cell(_:)
。
下面的例子是这个页面上方显示的示例的声明。
extension VenueDetailViewController: CellsDeclarator {
typealias CellDeclaration = CellBinder
func declareCells(_ cell: (CellDeclaration) -> Void) {
cell(VenueOutlineCell.makeBinder(value: venue))
if !reviews.isEmpty {
cell(SectionHeaderCell.makeBinder(value: "Reviews"))
for review in reviews {
cell(ReviewCell.makeBinder(value: review))
}
}
if !relatedVenues.isEmpty {
cell(SectionHeaderCell.makeBinder(value: "Related Venues"))
for relatedVenue in relatedVenues {
cell(RelatedVenueCell.makeBinder(value: relatedVenue))
}
}
}
}
在上面的代码中,假设 VenueOutlineCell、SectionHeaderCell、ReviewCell 和 RelatedVenueCell 都遵从 BindableCell 协议。
将.CollectionViewDataSource赋给 UICollectionView 的 dataSource
最后一步是创建 CollectionViewDataSource 的实例并将单元格声明赋给它。
final class VenueDetailViewController: UIViewController {
...
@IBOutlet private weak var collectionView: UICollectionView!
private let dataSource = CollectionViewDataSource()
override func viewDidLoad() {
super.viewDidLoad()
let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
collectionView.dataSource = dataSource
dataSource.cellDeclarations = cellDeclarations
}
}
视图控制器的 cellDeclarations 是从视图控制器中 declareCells(_:)
的结果计算得出的。当dataSource的cellDeclarations更新时,dataSource就准备好返回新的排列。然后,您可以调用 reloadData()
、reloadItems(at:)
、insertItems(at:)
和 deleteItems(at:)
中的任何一个来更新 UICollectionView。
高级用法
在某些体系结构(如 MVVM 和 VIPER)中,将逻辑与视图分离非常重要。DataSourceKit 有选项来引入这种分离。
通过枚举表达单元格声明
CellsDeclarator有一个名为CellDeclaration的类型参数,它表示单元格排列的元素类型。我们可以为这个类型参数指定任何类型,即使它是声明为struct或enum的原始数据。
例如,下面的实现是使用枚举声明单元格。
struct VenueDetailViewState {
var venue: Venue
var reviews: [Review]
var relatedVenues: [Venue]
}
extension VenueDetailViewState: CellsDeclarator {
enum CellDeclaration: Equatable {
case outline(Venue)
case sectionHeader(String)
case review(Review)
case relatedVenue(Venue)
}
func declareCells(_ cell: (CellDeclaration) -> Void) {
cell(.outline(venue))
if !reviews.isEmpty {
cell(.sectionHeader("Reviews"))
for review in reviews {
cell(.review(review))
}
}
if !relatedVenues.isEmpty {
cell(.sectionHeader("Related Venues"))
for relatedVenue in relatedVenues {
cell(.relatedVenue(relatedVenue))
}
}
}
}
由于VenueDetailViewState.CellDeclaration只是原始数据,因此很容易编写如下测试。
class VenueDetailViewStateTests: XCTestCase {
func testEmptyRelatedVenues() {
let venue = Venue(photo: nil, name: "Kaminarimon")
let review1 = Review(authorImage: nil, authorName: "Yosuke Ishikawa", body: "Foo")
let review2 = Review(authorImage: nil, authorName: "Masatake Yamoto", body: "Bar")
let data = VenueDetailViewState(
venue: venue,
reviews: [
review1,
review2,
],
relatedVenues: [])
XCTAssertEqual(data.cellDeclarations, [
.outline(venue),
.sectionHeader("Reviews"),
.review(review1),
.review(review2),
])
}
}
将枚举声明与单元格关联
CollectionViewDataSource也有一个名为CellDeclaration的类型参数。如果这个参数与CellBinder不同,CollectionDataSource的初始化器可接受一个函数(CellDeclaration) -> CellBinder,因为CollectionDataSource最终需要CellBinder来组装实际的单元格。
final class VenueDetailViewController: UIViewController {
@IBOutlet private weak var collectionView: UICollectionView!
private let dataSource = CollectionViewDataSource<VenueDetailViewState.CellDeclaration> { cellDeclaration in
switch cellDeclaration {
case .outline(let venue):
return VenueOutlineCell.makeBinder(value: venue)
case .sectionHeader(let title):
return SectionHeaderCell.makeBinder(value: title)
case .review(let review):
return ReviewCell.makeBinder(value: review)
case .relatedVenue(let venue):
return RelatedVenueCell.makeBinder(value: venue)
}
}
private var state = VenueDetailViewState() {
didSet {
collectionView.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
collectionView.dataSource = dataSource
dataSource.cellDeclarations = state.cellDeclarations
}
}