Skip to content

Just one possible approach to manage UITableView and UICollectionView data source

License

Notifications You must be signed in to change notification settings

devpolant/CellViewModel

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

64 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Swift Xcode MIT CocoaPods 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' do
  pod '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:
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!
}
  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):
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
  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

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(with: tableModel(at: indexPath), for: indexPath)
    }
    
    private func tableModel(at indexPath: IndexPath) -> AnyCellViewModel {
        return users[indexPath.row]
    }
}

Quick Setup

Use existed adapters in order to perform quick setup.

  1. For UITableView - TableViewDataAdapter
private lazy var adapter = TableViewDataAdapter(tableView: self.tableView)

// ...

func setup(users: [AnyCellViewModel]) {
    adapter.data = users
}

Updating data property will call reloadData().

  1. For UICollectionView - CollectionViewDataAdapter:
private lazy var adapter = 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

final class UsersViewController: BaseCollectionViewController {

    @IBOutlet weak var collectionView: UICollectionView {
        didSet {
            // initialize reference in base class
            _collectionView = collectionView
        }
    }
    
    override var viewModels: [AnyCellViewModel.Type] {
        return [
            UserCellModel.self,
            // ... add more
        ]
    }
    
    override var supplementaryModels: [AnySupplementaryViewModel.Type] {
        return [
            UserHeaderModel.self,
            /// ... add more
        ]
    }
    
    // ... your domain code

BaseCollectionViewController is a wrapper for CollectionViewDataAdapter, so it is already have setup method:

    open func setup(_ sections: [Section]) {
        adapter.data = sections
    }

Section type is a container for header, footer, items models and layout information like spacings etc.

public final class Section {
    
    public var insets: UIEdgeInsets?
    public var lineSpacing: CGFloat?
    public var header: AnySupplementaryViewModel?
    public var footer: AnySupplementaryViewModel?
    public var items: [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.

override var automaticallyInferCellViewModelTypes: Bool {
    return true
}

Accessibility

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

    // ...
}

License

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