SourceModel is a lightweight wrapper for UICollectionViewDataSource/Delegate and UITableViewDataSource/Delegate. It removes the burden of writing the boiler plate code when working with tableViews and collectionViews, allowing you to focus on what's important, populating the cells with data.
SourceModdel focus on Seperation of Concern, where the cell should only be accessible to get the data to present and decide how to present it. The dataSource should not be responsible for deciding how the cell should populate its data, nor know about the view.
Let's jump into some of the protocols you need to be familiar with, to get started. Let's think of a view, that shows products:
ModelCollection represent a collection of similar items, the products in our case.
publicprotocolModelCollection{
/// Returns the number of items
varnumberOfItems:Int{get}
/// Returns the number of sections
varnumberOfSections:Int{get}
/// A subscript to return a type in an indexPath
subscript(indexPath:IndexPath)->Model?{get}
/// A subscript to return a collection dataType within a section
subscript(section:Int)->ModelCollection{get}
/// Returns the cell type at indexPath
func cellType(forItemAt indexPath:IndexPath)->Fillable.Type?}Model represents a single item in a collection of item, a product.
Let's create a collection of items to see it in action:
We will dive into the
Fillableprotocol in a later stage, for now, let's assume we have aProductCell.
structProduct{...}structProducts:ModelCollection{letitems:[Product]varnumberOfItems:Int{return items.count
}varnumberOfSections:Int{return1}
subscript(indexPath:IndexPath)->Model?{returnitems[indexPath.row]}
subscript(section:Int)->ModelCollection{returnself}func cellType(forItemAt indexPath:IndexPath)->Fillable.Type?{returnProductCell.self
}}Ok, this is cool, but we can make it even simpler. SourceModel offers an Elements class that wrapps the above, where you only need to take care of the cells. This is how it works:
class Products: Elements<Product>, Codable {
func cellType(forItemAt indexPath: IndexPath) -> Fillable.Type? {
return ProductCell.self
}
}
Now that we we finalised our collection, let's create an instance of Products:
letproducts=Products(items:[Product(),Product()])Each cell must conform to the Fillable protocol. This is where your model is injected, and where you will populate the cell.
class ProductCell:UICollectionViewCell,Fillable{....func fill(with model:Model?){guardlet product = model as?Productelse{return}
/// Handle product
}}Now that we have the model, modelCollection, and the cell, let's create the DataSource and Delegate
classProductsViewController:UIViewController{vardataSource:CollectionDataSource! /// Use TableDataSource for UITableView
vardelegate:CollectionDelegate! /// Use TableDelegate for UITableView
letproducts=Products(items:[Product(),Product()])overridefunc viewDidLoad(){
super.viewDidLoad()setupCollectionView(with: products)}privatefunc setupCollectionView(with modelCollection:ModelCollection?)
delegate = CollectionDataSource(modelCollection:modelCollection)
dataSource = CollectionDelegate(modelCollection:modelCollection)
collectionView.register(cellType:ProductCell.self)
collectionView.delegate = delegate
collectionView.dataSource = dataSource
}}Note: To get the
register(cellType:)function, add StanwoodCore to your project.
That's it! 😊
You are probably wondering where to go from here? Let's look at a more complex cases, and how SourceModel can help you.
Working with sections can be a tedious task. You can use ModelCollection and inject a collection of sections (other ModelCollections). Alternatively, SourceModel offers Sections. Let's look at our Products elements from before, and migrate it to sections of different product categories
classProductsViewController:UIViewController{vardataSource:CollectionDataSource! /// Use TableDataSource for UITableView
vardelegate:CollectionDelegate! /// Use TableDelegate for UITableView
letsections:Sections!overridefunc viewDidLoad(){
super.viewDidLoad()letbeautyProducts=Products(items:[Product(),Product()])letgroceryProducts=Products(items:[Product(),Product()])
sections =Sections(sections:[beautyProducts, groceryProducts])setupCollectionView(with: products)}privatefunc setupCollectionView(with modelCollection:ModelCollection?)
delegate = CollectionDataSource(modelCollection:modelCollection)
dataSource = CollectionDelegate(modelCollection:modelCollection)
collectionView.register(cellType:ProductCell.self)
collectionView.delegate = delegate
collectionView.dataSource = dataSource
}}If you want to add a section header, make sure you ModelCollection conforms to Headerable. You have a couple of optional options, depending weather you are populating a collectionView or a tableView.
Headerable
/// Headerable protocol to add header support for `UICollectionView` and `UITableView`
@objcpublicprotocolHeaderable{
/// `UITableView` section header view
@objcoptionalvarheaderView:UIView{get}
/// `UICollectionView` header reusable header view
@objcoptionalvarreusableView:UICollectionReusableView{get}}Example
Let's take our Products and add a header
classProducts:Elements<Product>,Codable,Headerable{varheaderView:UIView{letview=CategoryHeaderView.loadFromNib()
view?.backgroundColor =.clear
view?.set(title: title)return view ??UIView()}...}Note: To get the
loadFromNib()function, add StanwoodCore to your project
Important!
Known issue: At this stage,
ModelCollectiondoes not support reusable header views. This is under development. To avoid any memory issues, you should useHeaderableonly for a single header view.
To work with the Delegate Design Pattern, SourceModel offers a Delegateable protocol. Let's see how that works with our ProductCell:
Example: Cell
protocolProductCellDelegate:class{func productCellDidDoSomething(with product:Product)}
/// Conform to `Delegateable`
classProductCell:UICollectionViewCell,Fillable,Delegateable{private weak vardelegate:ProductCellDelegate?....
/// Set the delegate
func set(delegate:AnyObject){self.delegate = delegate as?ProductCellDelegate}
@IBAction didTapSomeButton(_ sender: UIButton){
delegate?.productCellDidDoSomething(with: product)}}**Example: ViewController
classProductsViewController:UIViewController{vardataSource:CollectionDataSource! /// Use TableDataSource for UITableView
vardelegate:CollectionDelegate! /// Use TableDelegate for UITableView
letproducts=Products(items:[Product(),Product()])overridefunc viewDidLoad(){
super.viewDidLoad()setupCollectionView(with: products)}privatefunc setupCollectionView(with modelCollection:ModelCollection?)
delegate = CollectionDataSource(modelCollection:modelCollection)
/// Inject `self` as the delegate
dataSource = CollectionDelegate(modelCollection:modelCollection, delegate:self)
collectionView.register(cellType:ProductCell.self)
collectionView.delegate = delegate
collectionView.dataSource = dataSource
}}extension ProductsViewController: ProductCellDelegate {func productCellDidDoSomething(with product:Product){
/// Do something with product
}}In some cases, we require to know at what index the cell is positioned. Conform to Indexable protocol to get the index.
Example
class ProductCell:UICollectionViewCell,Fillable,Indexable{....func inject(_ indexPath:IndexPath){
/// Do something with the index
}}- Adding support for
delegatepatter - Adding support for
headers - Adding
Indexableprotocol to inject theindexPathinto the cell - Adding a
reloadData(animated:)that reloads the models that changed - Adding generics to
Fillableprotocol - Adding reusable
Header/Footerview - Adding
SwiftUIsupport - Adding
Combinesupport
Please open an issue for feature requests.
To run the example project, clone the repo, and run pod install from the Example directory first.
SourceModel is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod'SourceModel'Tal Zion, tal.zion@stanwood.de
SourceModel is available under the MIT license. See the LICENSE file for more info.
