Using CellViewModel to configure you UITableViewCell or UICollectionViewCell is just a one possible approach of work with UIKit's collections.
- iOS 10.0+
- Xcode 10.0+
- Swift 4.2+
target 'MyApp' do
pod 'CellViewModel', '~> 1.1'
end
github "AntonPoltoratskyi/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.
- You need to create cell class and appropriate type that conforms to CellViewModel type:
public typealias AnyViewCell = UIView
public protocol CellViewModel: AnyCellViewModel {
associatedtype Cell: AnyViewCell
func setup(cell: Cell)
}
UserTableViewCell.swift
import CellViewModel
// MARK: - View Model
struct UserCellModel: CellViewModel {
var user: User
func setup(cell: UserTableViewCell) {
cell.nameLabel.text = user.name
}
}
// MARK: - Cell
final class UserTableViewCell: UITableViewCell, XibInitializable {
@IBOutlet weak var nameLabel: UILabel!
}
- After that you need to register created model type:
There are 2 options:
- use
register(nibModel:)
if appropriateCellViewModel
'sCell
conforms toXibInitializable
:
tableView.register(nibModel: UserCellModel.self)
- otherwise use
register(viewModel:)
:
tableView.register(viewModel: UserCellModel.self)
- Then store your models in array (or your custom datasource type):
private var users: [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:
private var users: [CellViewModel] = [] // won't compile
- UITableViewDataSource implementation is very easy, even if you have multiple cell types, because all logic are contained in our view models:
import CellViewModel
class ViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
private var users: [AnyCellViewModel] = []
override func viewDidLoad() {
super.viewDidLoad()
users = User.testDataSource.map { UserCellModel(user: $0) }
tableView.register(nibModel: UserCellModel.self)
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return tableView.dequeueReusableCell(withModel: tableModel(at: indexPath), for: indexPath)
}
private func tableModel(at indexPath: IndexPath) -> AnyCellViewModel {
return users[indexPath.row]
}
}
- Or use TableViewDataAdapter:
private lazy var adapter = TableViewDataAdapter(tableView: self.tableView)
assign it as UITableView's dataSource:
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.dataSource = adapter
}
updating data
property will cause reloadData()
func setup(users: [AnyCellViewModel]) {
adapter.data = users
}
Sometimes there is a need to define accessibilityIdentifier
for UI testing purposes.
There is Accessible protocol that is conformed by CellViewModel protocol.
public protocol Accessible {
var accessibilityIdentifier: String? { get }
var accessibilityOptions: AccessibilityDisplayOptions { get }
}
So you need to define accessibilityIdentifier
property in your model type implementation:
struct UserCellModel: CellViewModel {
var accessibilityIdentifier: String? {
return "user_cell"
}
// ...
}
And define accessibilityOptions
if needed to add index path as suffix in the end of accessibilityIdentifier
:
struct UserCellModel: CellViewModel {
var accessibilityIdentifier: String? {
return "user_cell"
}
var accessibilityOptions: AccessibilityDisplayOptions {
return [.row, .section]
}
// ...
}
CellViewModel is available under the MIT license. See the LICENSE file for more info.