ComposableDataSource wraps the typically verbose UICollectionView data source and delegate implementation into a more neatly packed builder pattern
Chain your UICollectionView delegate calls one after another as needed:
let dataSource = ComposableCollectionDataSource(....)
// chain selection delegate function
// chain cell size delegate function
// ... and so on ...
- Xcode 8.0 or higher
- iOS 10.0 or higher
ComposableDataSource is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'ComposableDataSource'
There are three components to creating a ComposableDataSource:
- Setting up a View Model to represent and configure a cell
- Creating a Configurable Cell, using the BaseComposableCollectionViewCell superclass
- Creating a Composable Data Source
Technically, there's two steps here:
- Creating a View Model
- Creating a Model (Optional)
The view model decorates the cell with info from the model when the cell is dequeued with collectionView(_:cellForItemAt:)
.
Your View Model but conform to the BaseCollectionCellModel
protocol. Doing so will require you to return the UICollectionViewCell class
// View Model
struct ChatroomViewModel: BaseCollectionCellModel {
func getCellClass() -> AnyComposableCellClass {
return ChatroomCell.self
}
let chatroom: Chatroom
}
// Model
struct Chatroom {
// ...
}
Your View Model must conform to BaseCollectionCellModel
and provide a subclass of BaseComposableCollectionViewCell
subclass. Similar to how you would normally use collectionView.register(:forCellWithReuseIdentifier:)
class ChatroomCell: BaseComposableCollectionViewCell {
override func configure(with item: BaseCollectionCellModel, at indexPath: IndexPath) {
let chatroomViewModel = item as! ChatroomViewModel
let chatroom = chatroomViewModel.chatroom
// Decorate cell using chatroom object
}
// UIViews ...
override func setupUIElements() {
super.setupUIElements()
// Use `super.containerView` instead of contentView to add your subviews
}
}
Your UICollectionViewCell must subclass BaseComposableCollectionViewCell
. Additionally, take advantage of two overridable functions:
func configure(with item: BaseCollectionCellModel, at indexPath: IndexPath) {
}
And
func setupUIElements() {
}
The configure(with:at:)
function is an overridable function from the BaseComposableCollectionViewCell
that is automatically called when the cell is dequeued with collectionView(_:cellForItemAt:)
. Use this to decorate your cell with data.
The setupUIElements()
function is an overridable function from the BaseComposableCollectionViewCell
that is automatically called when the cell is initialized. Add your subviews, constraints, etc. here.
var dataSource: ComposableCollectionDataSource!
var collectionView : UICollectionView = ...
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
dataSource = setupDataSource()
}
....
private func setupDataSource() -> ComposableCollectionDataSource {
// Initialize double nested array of view models
// NOTE
// Each inner array represents each section of your data source
// in the order they are added
let models: [[ChatroomViewModel]] = [
// Section 0
[
ChatroomViewModel(chatroom: ....)
],
// Section 1, etc....
]
// Initialize array of supplementary models
// NOTE
// Each item represents the header and/or footer supplementary view
// for a specific section in the order they are added
let supplementaryModels: [GenericSupplementarySectionModel] = [....]
let dataSource = ComposableCollectionDataSource(collectionView: collectionView,
cellItems: models,
supplementarySectionItems: supplementaryModels)
.didSelectItem { (indexPath: IndexPath, model: BaseCollectionCellModel) in
// Handle selection at indexPath
}.sizeForItem { [unowned self] (indexPath: IndexPath, model: BaseCollectionCellModel) -> CGSize in
// Return size for cell at the specified indexPath
}.referenceSizeForHeader { [unowned self] (section: Int, model: BaseComposableSupplementaryViewModel) -> CGSize in
// Return size for supplementary header view at the specified indexPath
// If your data source will have supplementary models
}
// Chain more handlers ...
return dataSource
}
Adding new items the datasource is very straightforward with the public APIs offrered.
First, create new view models to represent the cells you want to add:
let newSectionOfItems: [ChatroomViewModel] = [
ChatroomViewModel(chatroom: ....),
// Array of items...
]
Then use the APIs provided by the ComposableCollectionDataSource
to add the items. In this example, we will insert a completely new section at section 0 (Essentially inserting at the top of the list and pushing any existing sections down, as expected)
let desiredSectionIndex: Int = 0
self.dataSource.insertNewSection(withCellItems: newSectionOfItems, supplementarySectionItem: nil,
atSection: desiredSectionIndex, completion: nil)
Additionally, inserting view models at varying indexPaths and indices is supported:
// Inserts cell view models at varying index paths
func insert(cellItems: [T], atIndexPaths indexPaths: [IndexPath],
updateStyle: DataSourceUpdateStyle, completion: OptionalCompletionHandler)
// Inserts supplementary section view models (Struct containing view models for header and/or footer supplementary views)
func insert(supplementarySectionItems: [S], atSections sections: [Int],
updateStyle: DataSourceUpdateStyle, completion: OptionalCompletionHandler)
Updating the datasource is similar to adding items. However, instead of providing indexPaths or section indices to insert items at, the values provided will update the existing items at said indexPaths/section indices. Example of updating sections:
let sectionIndicesToUpdate: [Int] = [0, 3]
let replacementItems: [[ChatroomViewModel]] = [
[
ChatroomViewModel(chatroom: ....),
// Array of items...
],
[
// Other items for next section in `sectionIndicesToUpdate`
]
]
self.dataSource.updateSections(atItemSectionIndices: sectionIndicesToUpdate,
newCellItems: replacementItems,
completion: nil)
Additionally, updating view models at varying indexPaths and indices is supported:
public func updateCellItems(atIndexPaths indexPaths: [IndexPath],
newCellItems: [T],
updateStyle: DataSourceUpdateStyle = .withBatchUpdates,
completion: OptionalCompletionHandler)
public func updateSupplementarySectionsItems(atSections sections: [Int],
withNewSupplementarySectionItems supplementarySectionItems: [S],
updateStyle: DataSourceUpdateStyle = .withBatchUpdates,
completion: OptionalCompletionHandler)
Deleting from the datasource can be done in multiple ways, deleting sections altogether:
let desiredSectionsToDelete: [Int] = [0, 2]
dataSource.deleteSections(atSectionIndices: desiredSectionsToDelete, completion: nil)
... or, by deleting cell view models at varying indexPaths, deleting supplementary section view models at varying sections
// Deletes cell view models at varying index paths
func deleteCellItems(atIndexPaths indexPaths: [IndexPath],
updateStyle: DataSourceUpdateStyle,
completion: OptionalCompletionHandler)
// Deletes supplementary section view models (Struct containing view models for header and/or footer supplementary views)
func deleteSupplementarySectionItems(atSections sections: [Int],
updateStyle: DataSourceUpdateStyle,
completion: OptionalCompletionHandler)
If you'd like to display some kind of view when the dataSource is empty:
let emptyView = UILabel()
emptyView.text = "Still loading data... :)"
emptyView.font = UIFont.boldSystemFont(ofSize: 25)
emptyView.numberOfLines = 0
emptyView.textAlignment = .center
dataSource.emptyDataSourceView = emptyView
Check out the full documentation here
To run the example project, clone the repo, and run pod install
from the Example directory first.
ChrishonWyllie, [email protected]
ComposableDataSource is available under the MIT license. See the LICENSE file for more info.