DataSourceKit 0.2.0

DataSourceKit 0.2.0

Ishikawa Yosuke 维护。



  • 作者
  • 石川义之,Ishikawa Yosuke

DataSourceKit

UICollectionView 和 UITableView 的声明式、可测试的数据源。

安装

有两种方法将 DataSourceKit 安装到您的项目中。

CocoaPods

在 Podfile 中添加一行 pod "DataSourceKit",然后运行 pod install

Carthage

在 Cartfile 中添加一行 github "ishkawa/DataSourceKit",然后运行 carthage update

简单用法

  1. 让单元格遵守 BindableCell。
  2. 让视图控制器遵守 CellsDeclarator。
  3. 创建一个 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
    }
}