Skip to content

Repository files navigation

SwiftXcodeMITCocoaPods Compatible

CellViewModel

Using CellViewModel to configure you UITableViewCell or UICollectionViewCell is just a one possible approach of work with UIKit's collections.

Requirements:

  • iOS 9.0+
  • Xcode 12.0+
  • Swift 5.0+

Installation

CocoaPods

target'MyApp'dopod'CellViewModel','~> 1.8.0'end

Carthage

github "devpolant/CellViewModel" "master"

Usage

Works with UITableView & UICollectionView - one possible approach, inspired by CocoaHeads:

You can move configuration logic for UITableViewCell or UICollectionViewCell from -cellForRowAtIndexPath: to separate types.

Native setup

  1. 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!}
  1. 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.

  1. 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
  1. 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]}}

Quick Setup

Use existed adapters in order to perform quick setup.

  1. For UITableView - TableViewDataAdapter
private lazy varadapter=TableViewDataAdapter(tableView:self.tableView)
// ...
func setup(users:[AnyCellViewModel]){
adapter.data = users
}

Updating data property will call reloadData().

  1. 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.

Base View Controller

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.

Usage

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}

Accessibility

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]}
// ...
}

License

CellViewModel is available under the MIT license. See the LICENSE file for more info.

About

Just one possible approach to manage UITableView and UICollectionView data source

Topics

Resources

Stars

10 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages