Using CellViewModel to configure you UITableViewCell or UICollectionViewCell is just a one possible approach of work with UIKit's collections.
- iOS 9.0+
- Xcode 12.0+
- Swift 5.0+
target'MyApp'dopod'CellViewModel','~> 1.8.0'endgithub "devpolant/CellViewModel" "master"
Works with UITableView & UICollectionView - one possible approach, inspired by CocoaHeads:
You can move configuration logic for UITableViewCell or UICollectionViewCell from -cellForRowAtIndexPath: to separate types.
- Create cell class and appropriate type that conforms to CellViewModel type:
publictypealiasAnyViewCell=UIViewpublicprotocolCellViewModel:AnyCellViewModel{associatedtypeCell:AnyViewCellfunc setup(cell:Cell)}UserTableViewCell.swift
import CellViewModel
// MARK: - View Model
structUserCellModel:CellViewModel{varuser:Userfunc setup(cell:UserTableViewCell){
cell.nameLabel.text = user.name
}}
// MARK: - Cell
finalclassUserTableViewCell:UITableViewCell,XibInitializable{@IBOutlet weak varnameLabel:UILabel!}- Register created model type:
tableView.register(UserCellModel.self)By registering model type it will be checked if cell class conforms to XibInitializable or not in order to register UINib or just cell's class type.
- Then store your models in array (or your custom datasource type):
privatevarusers:[AnyCellViewModel]=[]AnyCellViewModel is a base protocol of CellViewModel. It's needed only in order to fix compiler limitation as you can use protocols with associatedtype only as generic constraints and can't write something like this:
privatevarusers:[CellViewModel]=[] // won't compile- UITableViewDataSource implementation is very easy, even if you have multiple cell types, because all 'cellForRow' logic is contained in our view models:
import CellViewModel
classViewController:UIViewController{@IBOutlet weak vartableView:UITableView!privatevarusers:[AnyCellViewModel]=[]overridefunc viewDidLoad(){
super.viewDidLoad()
users =User.testDataSource.map{UserCellModel(user: $0)}
tableView.register(nibModel:UserCellModel.self)}}extensionViewController:UITableViewDataSource{func tableView(_ tableView:UITableView, numberOfRowsInSection section:Int)->Int{return users.count
}func tableView(_ tableView:UITableView, cellForRowAt indexPath:IndexPath)->UITableViewCell{return tableView.dequeueReusableCell(with:tableModel(at: indexPath), for: indexPath)}privatefunc tableModel(at indexPath:IndexPath)->AnyCellViewModel{returnusers[indexPath.row]}}Use existed adapters in order to perform quick setup.
- For
UITableView- TableViewDataAdapter
private lazy varadapter=TableViewDataAdapter(tableView:self.tableView)
// ...
func setup(users:[AnyCellViewModel]){
adapter.data = users
}Updating data property will call reloadData().
- For
UICollectionView- CollectionViewDataAdapter:
private lazy varadapter=CollectionViewDataAdapter(tableView:self.collectionView)
// ...
func setup(users:[AnyCellViewModel]){
adapter.data = users
}Both adapters already conform to appropriate datasource protocol: UICollectionViewDataSource and UITableViewDataSource.
The most simplier way to set up is to inherit from BaseCollectionViewController.
Sometimes you need a table UI, but with some unique section insets or interitem spacing. For this case BaseCollectionViewController provides default implementation of UICollectionViewDelegateFlowLayout protocol to match the table UI for you.
finalclassUsersViewController:BaseCollectionViewController{@IBOutlet weak varcollectionView:UICollectionView{
didSet {
// initialize reference in base class
_collectionView = collectionView
}}overridevarviewModels:[AnyCellViewModel.Type]{return[UserCellModel.self,
// ... add more
]}overridevarsupplementaryModels:[AnySupplementaryViewModel.Type]{return[UserHeaderModel.self,
/// ... add more
]}
// ... your domain code
BaseCollectionViewController is a wrapper for CollectionViewDataAdapter, so it is already have setup method:
openfunc setup(_ sections:[Section]){
adapter.data = sections
}Section type is a container for header, footer, items models and layout information like spacings etc.
publicfinalclassSection{publicvarinsets:UIEdgeInsets?publicvarlineSpacing:CGFloat?publicvarheader:AnySupplementaryViewModel?publicvarfooter:AnySupplementaryViewModel?publicvaritems:[AnyCellViewModel]
/// ...
}Override automaticallyInferCellViewModelTypes in order to allow to automatically infer type of used view models instead of explicitly declare them in viewModels and supplementaryModels properties.
overridevarautomaticallyInferCellViewModelTypes:Bool{returntrue}Sometimes there is a need to define accessibilityIdentifier for UI testing purposes.
There is Accessible protocol that is conformed by CellViewModel protocol.
publicprotocolAccessible{varaccessibilityIdentifier:String?{get}varaccessibilityOptions:AccessibilityDisplayOptions{get}}So you need to define accessibilityIdentifier property in your model type implementation:
structUserCellModel:CellViewModel{varaccessibilityIdentifier:String?{return"user_cell"}
// ...
}And define accessibilityOptions if needed to add index path as suffix in the end of accessibilityIdentifier:
structUserCellModel:CellViewModel{varaccessibilityIdentifier:String?{return"user_cell"}varaccessibilityOptions:AccessibilityDisplayOptions{return[.row,.section]}
// ...
}CellViewModel is available under the MIT license. See the LICENSE file for more info.