Доброго времени суток!
Недавно я начал переводить приложение, написанное по плохому MVC на VIPER. Это был мой первый опыт с VIPER-архитектурой и в силу того, что информации на просторах интернета на данный момент по этой архитектуре мало, я столкнулся с некоторыми проблемами. Используя самые общие знания и понятия по VIPER, я вывел на данный момент оптимальные для себя паттерны написания экранов, включающие в себя таблицы или коллекции.
Я разделяю ячейки на простые и сложные.
Простые ячейки — это такие ячейки, которым для выполнения своего предназначения достаточно отображать некоторые данные (текст, картинка) и отвечать простыми действиями на действия пользователя.
Сложные ячейки — такие ячейки, которым для выполнения своего предназначения необходимо дополнительно подгружать данные, которые имеют сложную бизнесс-логику внутри себя.
В данной статье речь пойдет о таблице с простыми ячейками.
Проблема заключается в том, что ячейку надо как-то собирать, как-то слушать ее события и делать это в правильном месте.
Для начала скажу, что некоторые могут предложить делать ячейки как отдельный модуль, но это очень не тривиальное и не особо оправданное решение, когда мы говорим о простых ячейках.
Будем разбирать все на примере. Пусть у нас есть список работников, у которых есть имя, специализация, фотография, места работы, ему можно написать письмо или же позвонить. Этот список мы хотим показать в UITableView в виде ячеек со всей описанной выше информацией и соответствующими кнопками для соответствующих действий.
Каждый работник будет одной секцией в таблице, каждый блок информации, красиво укладывающий в строку будет ячейкой этой секции.
Итак, что мы имеем:
Очевидно, обрабатывать события должен Presenter нашего основного модуля. Идея заключается в следующем:
Interactor модуля получает данные в обычном для него формате, передает их Presenter'у. Presenter в свою очередь должен из этих данных собрать данные понятные для View, в качестве таких данных я беру массив моделей секций, содержащие массив моделей строк. Модель секции имеет делегатом нашего Presenter'а, это нам понадобится для обработки событий. В свою очередь модель ячейки с кнопкой имеет блок обработки события кнопки, задающийся моделью секции, в которой она лежит. Таким образом, нажатие на кнопку в ячейке вызовет блок, в котором как к делегату секции выполнится обращение к Presenter'у, который в конце концов все и обработает.
Итак, ячейка есть элемент таблицы, которая есть View нашего модуля. По моему мнению, нет ничего удивительного и неправильного в том, что ее события обрабатываются Presenter'ом того же модуля. Можно рассматривать модели ячеек и секций как вариант примитивнейшего Presenter'а нашей ячейки, которому ничего не надо подгружать и вся информация для работы ему дается извне. Тогда модуль ячейки это простейший модуль, состоящий только из View и Presenter. Реализация такого «модуля» будет немного не такой, как реализация нормального модуля: на то я его так и не называю.
Реализация будет построена на использовании полиморфизма через протоколы.
Начнем с протоколов, без которых все было бы не так красиво.
Протокол, который будут реализовывать все модели ячеек:
Протокол, который будут реализовывать все ячейки с моделями:
Протокол, который будут реализовывать все модели секций:
Теперь создадим необходимые нам модели ячеек.
1. Так как все ячейки будут иметь автоматическую высоту, то сначала создадим базовый класс для всех моделей, где это укажем.
2. Модель ячейки, отображающая фото, имя и специализацию работника.
3. Модель ячейки, отображающая место работы работника.
4. Модель ячейки с кнопкой
С моделями ячеек закончили. Создадим классы ячеек.
1. Базовый класс
Как видно по коду, настройка UI ячейки произойдет, как только ей отдадут ее модель.
2. Класс ячейки базовой информации работника.
3. Класс ячейки отображающей место работы
4. Класс ячейки с кнопкой
Закончили с ячейками. Перейдем к модели секции.
Здесь и происходит связывания действий над ячейками с Presenter'ом.
Осталось самое простое — отобразить данные в таблице.
Для этого сначала создадим прототипы ячеек в нашей таблице и дадим им соотвествующие identifier'ы.
Результат будет выглядеть примерно так. Необходимо проставить всем ячейкам их классы и reuse identifier'ы и соединить все аутлеты.
Теперь соберем секции в Presenter'а на основе полученных данных от Interactor'а и отдадим массив секций View для отображения.
Так выглядит наш Presenter:
И так прекрасно выглядит наш View:
А вот так выглядит результат (я немного навел красоты, о которой здесь не написал):
Мы получили очень гибкую реализацию поставленной цели: модели дают возможность очень быстро убрать или добавить нужную ячейку, не трогая при этом View и меняя только небольшие и кусочки кода.
Можно расширять модели чем угодно, чтобы не загрязнять ваш View. Например, если вам надо отключить выделение только для конкретных ячеек, вы можете добавить соответствующую проперти в модель и настраивать впоследствии ячейку в описанном выше методе.
Это моя текущая реализация, если кто-то готов предложить что-то более красивое, правильное и удобное — я только рад! В следующих статьях постараюсь рассказать о реализации сложных ячеек (когда сам найду что-то удобное).
Недавно я начал переводить приложение, написанное по плохому 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. Например, если вам надо отключить выделение только для конкретных ячеек, вы можете добавить соответствующую проперти в модель и настраивать впоследствии ячейку в описанном выше методе.
Это моя текущая реализация, если кто-то готов предложить что-то более красивое, правильное и удобное — я только рад! В следующих статьях постараюсь рассказать о реализации сложных ячеек (когда сам найду что-то удобное).
Ссылка на репозиторий проекта
Комментарии (2)
Gallion
24.01.2018 10:05Презентер ничего и не знает о представлении. Таким же образом вью контроллер, который по сути является вью, общается с презентером через ViewOutput. Здесь презентер также реализует протокол EmployeeSectionModelDelegate и просто может реагировать на события из вью, в данном случае ячейки таблицы, но сам презентер ничего о представлении не знает.
Adnako
А почему user interaction обрабатывается presenter’ом?
Получается, что Presenter знает про UIKit.