Good day!
Recently I started translating an application written in bad MVC 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 about this architecture, I ran into some problems. Using the most general knowledge and concepts on VIPER, I have derived for the moment the optimal patterns for writing screens, including tables or collections.
Simple and complex cells
I divide cells into simple and complex.
')
Simple cells are such 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 such cells that, in order to fulfill their mission, it is necessary 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 collected, somehow listen to its events and do it in the right place.
Decision
To begin with, I will say that some may suggest making cells as a separate module, but this is not a very trivial and not particularly justified decision when we are talking about simple cells.
We will analyze all the example. Suppose we have a list of employees who have a name, specialization, photograph, place of work, you can write a letter or call. We want to show this list in UITableView as cells with all the information described above and the corresponding buttons for the corresponding actions.
Each worker will be one section in the table, each block of information beautifully stacked in a row will be a cell of this section.
So, what we have:
- In the section there are cells that display some information.
- In the section there are two buttons whose events need to be processed.
Obviously, the Presenter of our main module should process events. The idea is this:
The module's Interactor receives data in its usual format, sends them to the Presenter. Presenter, in turn, should collect data that is understandable for View from this data; I take an array of section models containing an array of string models as such data. The section model has a delegate of our Presenter, we need it for event handling. In turn, the cell model with a button has a button event handling unit defined by the section model in which it lies. Thus, clicking on a button in a cell will cause a block in which, as a delegate of a section, a call will be made to the Presenter, which will ultimately handle everything.

So, the cell is an element of the table, which is the View of our module. In my opinion, it is not surprising and wrong that its events are handled by the Presenter of the same module. We can consider the cell and section models as a variant of the 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 bit different from 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 we will create the necessary cell models.
1. Since all cells will have an automatic height, we first create a base class for all models, where we specify it.
class EmployeeBaseCellModel: CellIdentifiable { let automaticHeight: Float = -1.0 var cellIdentifier: String { return "" } var cellHeight: Float { return automaticHeight } }
2. A 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. Cell model, showing 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. Cell model with button
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 classes of cells.
1. Base class
class EmployeeBaseCell: UITableViewCell, ModelRepresentable { var model: CellIdentifiable? { didSet { updateViews() } } func updateViews() { } }
As you can see by the code, the UI setting of the cell will occur as soon as it is given its model.
2. The cell class of employee 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. The class of the cell showing the 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. Cell class with button
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. We turn to the model section.
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]) } }
Here is the binding of actions on the cells with Presenter.
The simplest thing is to display the data in the table.
To do this, first create prototype cells in our table and give them the appropriate identifiers.
The result will look something like this. It is necessary to put down all the cells of their classes and reuse identifiers, and connect all the outlets.

Now we will assemble the sections in Presenter based on the data obtained from Interactor and give the array of the View sections for display.
This is how 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 so our View looks great:
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, about which I did not write here):

Total
We got a very flexible implementation of the goal: the models make it possible to quickly remove or add the desired cell, without touching the View and changing only small and code pieces.
You can expand the model with anything to not pollute your View. For example, if you need to turn off selection for specific cells only, you can add the appropriate property to the model and set up the cell afterwards in the method described above.
This is my current implementation, if someone is ready to offer something more beautiful, correct and convenient - I'm just glad! In the following articles I will try to talk about the implementation of complex cells (when I myself find something convenient).