Недавно я начал переводить приложение, написанное по плохому MVC на VIPER. Это был мой первый опыт с VIPER-архитектурой и в силу того, что информации на просторах интернета на данный момент по этой архитектуре мало, я столкнулся с некоторыми проблемами. Используя самые общие знания и понятия по VIPER, я вывел на данный момент оптимальные для себя паттерны написания экранов, включающие в себя таблицы или коллекции.
Простые и сложные ячейки
Я разделяю ячейки на простые и сложные.
Простые ячейки — это такие ячейки, которым для выполнения своего предназначения достаточно отображать некоторые данные (текст, картинка) и отвечать простыми действиями на действия пользователя.
Сложные ячейки — такие ячейки, которым для выполнения своего предназначения необходимо дополнительно подгружать данные, которые имеют сложную бизнесс-логику внутри себя.
В данной статье речь пойдет о таблице с простыми ячейками.
Проблема
Проблема заключается в том, что ячейку надо как-то собирать, как-то слушать ее события и делать это в правильном месте.
Решение
Для начала скажу, что некоторые могут предложить делать ячейки как отдельный модуль, но это очень не тривиальное и не особо оправданное решение, когда мы говорим о простых ячейках.
Будем разбирать все на примере. Пусть у нас есть список работников, у которых есть имя, специализация, фотография, места работы, ему можно написать письмо или же позвонить. Этот список мы хотим показать в UITableView в виде ячеек со всей описанной выше информацией и соответствующими кнопками для соответствующих действий.
Каждый работник будет одной секцией в таблице, каждый блок информации, красиво укладывающий в строку будет ячейкой этой секции.
Итак, что мы имеем:
- В секции есть ячейки, отображающие некоторую информацию
- В секции есть две кнопки, события которых необходимо обрабатывать
Очевидно, обрабатывать события должен Presenter нашего основного модуля. Идея заключается в следующем:
Interactor модуля получает данные в обычном для него формате, передает их Presenter'у. Presenter в свою очередь должен из этих данных собрать данные понятные для View, в качестве таких данных я беру массив моделей секций, содержащие массив моделей строк. Модель секции имеет делегатом нашего Presenter'а, это нам понадобится для обработки событий. В свою очередь модель ячейки с кнопкой имеет блок обработки события кнопки, задающийся моделью секции, в которой она лежит. Таким образом, нажатие на кнопку в ячейке вызовет блок, в котором как к делегату секции выполнится обращение к Presenter'у, который в конце концов все и обработает.

Итак, ячейка есть элемент таблицы, которая есть View нашего модуля. По моему мнению, нет ничего удивительного и неправильного в том, что ее события обрабатываются Presenter'ом того же модуля. Можно рассматривать модели ячеек и секций как вариант примитивнейшего Presenter'а нашей ячейки, которому ничего не надо подгружать и вся информация для работы ему дается извне. Тогда модуль ячейки это простейший модуль, состоящий только из View и Presenter. Реализация такого «модуля» будет немного не такой, как реализация нормального модуля: на то я его так и не называю.
Реализация будет построена на использовании полиморфизма через протоколы.
Начнем с протоколов, без которых все было бы не так красиво.
Протокол, который будут реализовывать все модели ячеек:
protocol CellIdentifiable { var cellIdentifier: String { get } var cellHeight: Float { get } }
Протокол, который будут реализовывать все ячейки с моделями:
protocol ModelRepresentable { var model: CellIdentifiable? { get set } }
Протокол, который будут реализовывать все модели секций:
protocol SectionRowsRepresentable { var rows: [CellIdentifiable] { get set } }
Теперь создадим необходимые нам модели ячеек.
1. Так как все ячейки будут иметь автоматическую высоту, то сначала создадим базовый класс для всех моделей, где это укажем.
class EmployeeBaseCellModel: CellIdentifiable { let automaticHeight: Float = -1.0 var cellIdentifier: String { return "" } var cellHeight: Float { return automaticHeight } }
2. Модель ячейки, отображающая фото, имя и специализацию работника.
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. Модель ячейки, отображающая место работы работника.
class EmployeeWorkplaceCellModel: EmployeeBaseCellModel { override var cellIdentifier: String { return "EmployeeWorkplaceCell" } var workplace: String init(_ workplace: String) { self.workplace = workplace } }
4. Модель ячейки с кнопкой
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 } }
С моделями ячеек закончили. Создадим классы ячеек.
1. Базовый класс
class EmployeeBaseCell: UITableViewCell, ModelRepresentable { var model: CellIdentifiable? { didSet { updateViews() } } func updateViews() { } }
Как видно по коду, настройка UI ячейки произойдет, как только ей отдадут ее модель.
2. Класс ячейки базовой информации работника.
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 EmployeeWorkplaceCell: EmployeeBaseCell { @IBOutlet weak var workplaceLabel: UILabel! override func updateViews() { guard let model = model as? EmployeeWorkplaceCellModel else { return } workplaceLabel.text = model.workplace } }
4. Класс ячейки с кнопкой
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?() } }
Закончили с ячейками. Перейдем к модели секции.
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]) } }
Здесь и происходит связывания действий над ячейками с Presenter'ом.
Осталось самое простое — отобразить данные в таблице.
Для этого сначала создадим прототипы ячеек в нашей таблице и дадим им соотвествующие identifier'ы.
Результат будет выглядеть примерно так. Необходимо проставить всем ячейкам их классы и reuse identifier'ы и соединить все аутлеты.

Теперь соберем секции в Presenter'а на основе полученных данных от Interactor'а и отдадим массив секций View для отображения.
Так выглядит наш Presenter:
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)") } }
И так прекрасно выглядит наш View:
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) } }
А вот так выглядит результат (я немного навел красоты, о которой здесь не написал):

Итог
Мы получили очень гибкую реализацию поставленной цели: модели дают возможность очень быстро убрать или добавить нужную ячейку, не трогая при этом View и меняя только небольшие и кусочки кода.
Можно расширять модели чем угодно, чтобы не загрязнять ваш View. Например, если вам надо отключить выделение только для конкретных ячеек, вы можете добавить соответствующую проперти в модель и настраивать впоследствии ячейку в описанном выше методе.
Это моя текущая реализация, если кто-то готов предложить что-то более красивое, правильное и удобное — я только рад! В следующих статьях постараюсь рассказать о реализации сложных ячеек (когда сам найду что-то удобное).

