Declarative, testable data source of UICollectionView and UITableView.
There are 2 ways to install DataSourceKit to your project.
Add a line pod "DataSourceKit" to your Podfile and run pod install.
Add a line github "ishkawa/DataSourceKit" to your Cartfile and run carthage update.
- Let cells to conform BindableCell.
- Let view controllers to conform CellsDeclarator.
- Create an instance of CollectionViewDataSource and assign it to dataSource of UICollectionView.
To make cells available in DataSourceKit machanism, let cells to conform BindableCell. BindableCell is a protocol which provide interfaces for registering cell to UICollectionView and binding cell with value.
For example, following implementation indicates that ReviewCell will be registered by UINib named "ReviewCell" and reuse identifier "Review", and the cell will be binded with value Review.
extensionReviewCell:BindableCell{staticfunc makeBinder(value review:Review)->CellBinder{returnCellBinder(
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
})}}Next step is to declare arrangement of cells. CellsDeclarator is a protocol to do this.
Suppose we have following data in a view controller:
finalclassVenueDetailViewController:UIViewController{varvenue:Venuevarreviews:[Review]varrelatedVenues:[Venue]}To declare arrangement of cells, pass makeBinder(value:) of cells to cell(_:) in declareCells(_:). Since call of cell(_:) will be converted to actual cell, call cell(_:) in the same order as you want to display cells.
Following example is the declaration of the demo which is displayed on the top of this page.
extensionVenueDetailViewController:CellsDeclarator{typealiasCellDeclaration=CellBinderfunc declareCells(_ cell:(CellDeclaration)->Void){cell(VenueOutlineCell.makeBinder(value: venue))if !reviews.isEmpty {cell(SectionHeaderCell.makeBinder(value:"Reviews"))forreviewin reviews {cell(ReviewCell.makeBinder(value: review))}}if !relatedVenues.isEmpty {cell(SectionHeaderCell.makeBinder(value:"Related Venues"))forrelatedVenuein relatedVenues {cell(RelatedVenueCell.makeBinder(value: relatedVenue))}}}}In above code, it is assumed that VenueOutlineCell, SectionHeaderCell, ReviewCell and RelatedVenueCell conform to BindableCell protocol.
Final step is creating instance of CollectionViewDataSource and assigning cell declarations to it.
finalclassVenueDetailViewController:UIViewController{...@IBOutletprivate weak varcollectionView:UICollectionView!privateletdataSource=CollectionViewDataSource()overridefunc viewDidLoad(){
super.viewDidLoad()letlayout= collectionView.collectionViewLayout as!UICollectionViewFlowLayout
layout.estimatedItemSize =UICollectionViewFlowLayout.automaticSize
collectionView.dataSource = dataSource
dataSource.cellDeclarations = cellDeclarations
}}cellDeclarations of view controller is computed from result of declareCells(_:) of the view controller. When cellDeclarations of dataSource is updated, dataSource is ready for returning new arrangement. Then, you can invoke update of UICollectionView by any of reloadData(), reloadItems(at:), insertItems(at:) and deleteItems(at:).
With some architectures such as MVVM and VIPER, it is important to separate logic from view. DataSourceKit has option to introduce the separation.
CellsDeclarator have a type parameter named CellDeclaration, which represents type of element of cells arrangement. We can specify any type for this type parameter, even if it is an plain data which declared as struct or enum.
For example, following implementation declares cells by enum.
structVenueDetailViewState{varvenue:Venuevarreviews:[Review]varrelatedVenues:[Venue]}extensionVenueDetailViewState:CellsDeclarator{enumCellDeclaration: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"))forreviewin reviews {cell(.review(review))}}if !relatedVenues.isEmpty {cell(.sectionHeader("Related Venues"))forrelatedVenuein relatedVenues {cell(.relatedVenue(relatedVenue))}}}}Since VenueDetailViewState.CellDeclaration is just a plain data, it is easy to write test like below:
classVenueDetailViewStateTests:XCTestCase{func testEmptyRelatedVenues(){letvenue=Venue(photo:nil, name:"Kaminarimon")letreview1=Review(authorImage:nil, authorName:"Yosuke Ishikawa", body:"Foo")letreview2=Review(authorImage:nil, authorName:"Masatake Yamoto", body:"Bar")letdata=VenueDetailViewState(
venue: venue,
reviews:[
review1,
review2,],
relatedVenues:[])XCTAssertEqual(data.cellDeclarations,[.outline(venue),.sectionHeader("Reviews"),.review(review1),.review(review2),])}}CollectionViewDataSource has a type parameter named CellDeclaration too. If this parameter differs from CellBinder, initializer of CollectionViewDataSource takes a function (CellDeclaration) -> CellBinder, because CollectionViewDataSource finally needs CellBinder to assemble actual cells.
finalclassVenueDetailViewController:UIViewController{@IBOutletprivate weak varcollectionView:UICollectionView!privateletdataSource= CollectionViewDataSource<VenueDetailViewState.CellDeclaration>{ cellDeclaration inswitch cellDeclaration {case.outline(let venue):returnVenueOutlineCell.makeBinder(value: venue)case.sectionHeader(let title):returnSectionHeaderCell.makeBinder(value: title)case.review(let review):returnReviewCell.makeBinder(value: review)case.relatedVenue(let venue):returnRelatedVenueCell.makeBinder(value: venue)}}privatevarstate=VenueDetailViewState(){
didSet {
collectionView.reloadData()}}overridefunc viewDidLoad(){
super.viewDidLoad()letlayout= collectionView.collectionViewLayout as!UICollectionViewFlowLayout
layout.estimatedItemSize =UICollectionViewFlowLayout.automaticSize
collectionView.dataSource = dataSource
dataSource.cellDeclarations = state.cellDeclarations
}}