VIPER and UITableView / UICollectionView with simple cells

Good day!

I recently started translating a bad MVC application to VIPER. This was my first experience with VIPER architecture and due to the fact that there is little information on the Internet at the moment on this architecture, I ran into some problems. Using the most general knowledge and concepts on VIPER, I have now deduced screen writing patterns that are optimal for myself, including tables or collections.

Simple and complex cells


I divide the cells into simple and complex.

Simple cells are those cells that, in order to fulfill their purpose, it is enough to display some data (text, picture) and respond with simple actions to user actions.

Complex cells are those cells that, in order to fulfill their purpose, need to additionally load data that has complex business logic within itself.

This article will focus on a table with simple cells.

Problem


The problem is that the cell must somehow be assembled, somehow listen to its events and do it in the right place.

Decision


To begin with, I will say that some may offer to make cells as a separate module, but this is not a very trivial and not particularly justified solution when we talk about simple cells.

We will analyze everything with an example. Suppose we have a list of employees who have a name, specialization, photograph, place of work, you can write him a letter or call. We want to show this list in a UITableView in the form of cells with all the information described above and the corresponding buttons for the corresponding actions.
Each employee will be one section in the table, each block of information that beautifully fits into a row will be a cell of this section.

So what we have:

  • The section has cells displaying some information.
  • There are two buttons in the section, the events of which must be processed

Obviously, the Presenter of our main module should handle events. The idea is as follows: The

module’s Interactor receives data in its usual format, passes it to Presenter. Presenter, in turn, must collect data that is understandable for View from this data. As such data, I take an array of section models containing an array of row models. The section model has a delegate of our Presenter, we will need this for event processing. In turn, the model of the cell with the button has a button for processing the event of the button, which is defined by the model of the section in which it lies. Thus, clicking on the button in the cell will cause a block in which, as a section delegate, an appeal will be made to the Presenter, which in the end will process everything.

image

So, the cell is an element of the table, which is the View of our module. In my opinion, there is nothing surprising or wrong in that its events are processed by the Presenter of the same module. You can consider the models of cells and sections as a variant of the most primitive Presenter of our cell, which does not need to load anything and all the information for work is given to it from the outside. Then the cell module is the simplest module, consisting only of View and Presenter. The implementation of such a “module” will be a little different than the implementation of a normal module: I don’t call it that.

The implementation will be built on the use of polymorphism through protocols.

Let's start with the protocols, without which everything would not be so beautiful.

The protocol that all cell models will implement:

protocol CellIdentifiable {
    var cellIdentifier: String { get }
    var cellHeight: Float { get }
}

The protocol that all cells with models will implement:

protocol ModelRepresentable {
    var model: CellIdentifiable? { get set }
}

The protocol that all section models will implement:

protocol SectionRowsRepresentable {
    var rows: [CellIdentifiable] { get set }
}

Now create the cell models we need.

1. Since all cells will have automatic height, we first create a base class for all models, where we indicate this.

class EmployeeBaseCellModel: CellIdentifiable {
    let automaticHeight: Float = -1.0
    var cellIdentifier: String {
        return ""
    }
    var cellHeight: Float {
        return automaticHeight
    }
}

2. The cell model that displays the photo, name and specialization of the employee.

class EmployeeBaseInfoCellModel: EmployeeBaseCellModel {
    override var cellIdentifier: String {
        return "EmployeeBaseInfoCell"
    }
    var name: String
    var specialization: String
    var imageURL: URL?
    init(_ employee: Employee) {
        name = employee.name
        specialization = employee.specialization
        imageURL = employee.imageURL
    }
}

3. The cell model that displays the place of work of the employee.

class EmployeeWorkplaceCellModel: EmployeeBaseCellModel {
    override var cellIdentifier: String {
        return "EmployeeWorkplaceCell"
    }
    var workplace: String
    init(_ workplace: String) {
        self.workplace = workplace
    }
}

4. Button cell model

class ButtonCellModel: EmployeeBaseCellModel {
    typealias ActionHandler = () -> ()
    override var cellIdentifier: String {
        return "ButtonCell"
    }
    var action: ActionHandler?
    var title: String
    init(title: String, action: ActionHandler? = nil) {
        self.title = title
        self.action = action
    }
}

With cell models finished. Create cell classes.

1. Base class

class EmployeeBaseCell: UITableViewCell, ModelRepresentable {
    var model: CellIdentifiable? {
        didSet {
            updateViews()
        }
    }
    func updateViews() {
    }
}

As you can see from the code, the UI configuration of the cell will occur as soon as its model is given to it.

2. The cell class of the employee’s basic information.

class EmployeeBaseInfoCell: EmployeeBaseCell {
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var specializationLabel: UILabel!
    @IBOutlet weak var photoImageView: UIImageView!
    override func updateViews() {
        guard let model = model as? EmployeeBaseInfoCellModel else {
            return
        }
        nameLabel.text = model.name
        specializationLabel.text = model.specialization
        if let imagePath = model.imageURL?.path {
            photoImageView.image = UIImage(contentsOfFile: imagePath)
        }
    }
}

3. Class of the cell displaying the place of work

class EmployeeWorkplaceCell: EmployeeBaseCell {
    @IBOutlet weak var workplaceLabel: UILabel!
    override func updateViews() {
        guard let model = model as? EmployeeWorkplaceCellModel else {
            return
        }
        workplaceLabel.text = model.workplace
    }
}

4. Button cell class

class ButtonCell: EmployeeBaseCell {
    @IBOutlet weak var button: UIButton!
    override func updateViews() {
        guard  let model = model as? ButtonCellModel else {
            return
        }
        button.setTitle(model.title, for: .normal)
    }
    @IBAction func buttonAction(_ sender: UIButton) {
        guard  let model = model as? ButtonCellModel else {
            return
        }
        model.action?()
    }
}

Finished with cells. Let's move on to the section model.

protocol EmployeeSectionModelDelegate: class {
    func didTapCall(withPhone phoneNumber: String)
    func didTapText(withEmail email: String)
}
class EmployeeSectionModel: SectionRowsRepresentable {
    var rows: [CellIdentifiable]
    weak var delegate: EmployeeSectionModelDelegate?
    init(_ employee: Employee) {
        rows = [CellIdentifiable]()
        rows.append(EmployeeBaseInfoCellModel(employee))
        rows.append(contentsOf: employee.workplaces.map({ EmployeeWorkplaceCellModel($0) }))
        let callButtonCellModel = ButtonCellModel(title: "Позвонить") { [weak self] in
            self?.delegate?.didTapCall(withPhone: employee.phone)
        }
        let textButtonCellModel = ButtonCellModel(title: "Написать письмо") { [weak self] in
            self?.delegate?.didTapText(withEmail: employee.email)
        }
        rows.append(contentsOf: [callButtonCellModel, textButtonCellModel])
    }
}

This is where the actions on the cells are associated with the Presenter.

The simplest thing left is to display the data in the table.
To do this, first create the prototypes of the cells in our table and give them the appropriate identifiers.

The result will look something like this. It is necessary to affix all classes with their classes and reuse identifiers and connect all outlets.

image

Now we will assemble sections in Presenter based on the received data from Interactor and give an array of View sections for display.

This is what our Presenter looks like:

class EmployeeListPresenter: EmployeeListModuleInput, EmployeeListViewOutput, EmployeeListInteractorOutput {
    weak var view: EmployeeListViewInput!
    var interactor: EmployeeListInteractorInput!
    var router: EmployeeListRouterInput!
    func viewDidLoad() {
        interactor.getEmployees()
    }
    func employeesDidReceive(_ employees: [Employee]) {
        var sections = [EmployeeSectionModel]()
        employees.forEach({
            let section = EmployeeSectionModel($0)
            section.delegate = self
            sections.append(section)
        })
        view.updateForSections(sections)
    }
}
extension EmployeeListPresenter: EmployeeSectionModelDelegate {
    func didTapText(withEmail email: String) {
        print("Will text to \(email)")
    }
    func didTapCall(withPhone phoneNumber: String) {
        print("Will call to \(phoneNumber)")
    }
}

And our View looks so beautiful:

class EmployeeListViewController: UITableViewController, EmployeeListViewInput {
    var output: EmployeeListViewOutput!
    var sections = [EmployeeSectionModel]()
    override func viewDidLoad() {
        super.viewDidLoad()
        output.viewDidLoad()
    }
    func updateForSections(_ sections: [EmployeeSectionModel]) {
        self.sections = sections
        tableView.reloadData()
    }
}
extension EmployeeListViewController {
    override func numberOfSections(in tableView: UITableView) -> Int {
        return sections.count
    }
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return sections[section].rows.count
    }
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let model = sections[indexPath.section].rows[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: model.cellIdentifier, for: indexPath) as! EmployeeBaseCell
        cell.model = model
        return cell
    }
    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return CGFloat(sections[indexPath.section].rows[indexPath.row].cellHeight)
    }
}

And here is the result (I brought a little beauty, which I did not write about here):

image

Total


We got a very flexible implementation of the goal: the models make it possible to very quickly remove or add the desired cell, without touching the View and changing only small and pieces of code.

You can expand your models in any way so as not to pollute your View. For example, if you need to disable selection only for specific cells, you can add the appropriate property to the model and subsequently configure the cell in the method described above.
This is my current implementation, if someone is ready to offer something more beautiful, correct and convenient - I'm only happy! In the following articles I will try to talk about the implementation of complex cells (when I find something convenient myself).

Link to the project repository


Also popular now: