
Введение
UITableView один из самых часто используемых компонентов UIKit. Табличное представление зарекомендовало себя как одно из самых удобных взаимодействий пользователя с контентом представленным на экране смартфона.
На сегодняшний день, каждому iOS разработчику необходимо в совершенстве владеть UITableView, знать тонкости и понимать как его адаптировать под разные архитектуры, чтобы использование не вызывало лишних проблем и трудностей.
В этой статье мы поговорим о том, как адаптировать UITableView под архитектуру Model-View-ViewModel (MVVM). Начнём.
Содержание
-
Введение
-
Пример
-
Реализация
-
Использование
-
Результат
-
Вывод
Пример
В качестве примера я реализовал ячейку с кнопкой, картинкой и текстом.

Реализация
Первым делом создадим подкласс от UITableView и назовем его AdaptedTableView.
class AdaptedTableView: UITableView { }
Определим метод setup(). Он необходим для конфигурации таблицы. Временно заполним обязательные для реализации методы UITableViewDataSource.
class AdaptedTableView: UITableView { // MARK: - Public methods func setup() { self.dataSource = self } } // MARK: - UITableViewDataSource extension AdaptedTableView: UITableViewDataSource { func numberOfSections(in tableView: UITableView) -> Int { .zero } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { .zero } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { UITableViewCell() } }
Согласно паттерну MVVM, view владеет viewModel. Создадим абстракцию для входных данных и назовем её AdaptedViewModelInputProtocol. AdaptedSectionViewModelProtocol необходим для описания viewModel секции. AdaptedCellViewModelProtocol служит лишь для полиморфизма подтипов наших viewModels для ячеек.
protocol AdaptedCellViewModelProtocol { } protocol AdaptedSectionViewModelProtocol { var cells: [AdaptedCellViewModelProtocol] { get } } protocol AdaptedViewModelInputProtocol { var sections: [AdaptedSectionViewModelProtocol] { get } }
Добавляем viewModel. Теперь у нас есть возможность корректно заполнить методы UITableViewDataSource.
class AdaptedTableView: UITableView { // MARK: - Public properties var viewModel: AdaptedViewModelInputProtocol? // MARK: - Public methods func setup() { self.dataSource = self } } // MARK: - UITableViewDataSource extension AdaptedTableView: UITableViewDataSource { func numberOfSections(in tableView: UITableView) -> Int { viewModel?.sections.count ?? .zero } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { viewModel?.sections[section].cells.count ?? .zero } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cellViewModel = viewModel?.sections[indexPath.section].cells[indexPath.row] else { return UITableViewCell() } // TO DO: - Register cell // TO DO: - Create cell return UITableViewCell() } }
На данном этапе с AdaptedTableView почти все готов, однако есть еще пару нерешенных вопросов. Регистрация и переиспользование ячеек. Создадим протокол AdaptedCellProtocol, который будут реализовывать все наши подклассы UITableViewCell, добавим метод register(_ tableView:) и reuse(_ tableView:, for indexPath:).
protocol AdaptedCellProtocol { static var identifier: String { get } static var nib: UINib { get } static func register(_ tableView: UITableView) static func reuse(_ tableView: UITableView, for indexPath: IndexPath) -> Self } extension AdaptedCellProtocol { static var identifier: String { String(describing: self) } static var nib: UINib { UINib(nibName: identifier, bundle: nil) } static func register(_ tableView: UITableView) { tableView.register(nib, forCellReuseIdentifier: identifier) } static func reuse(_ tableView: UITableView, for indexPath: IndexPath) -> Self { tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) as! Self } }
Для порождения ячеек создадим протокол фабричного метода AdaptedCellFactoryProtocol.
protocol AdaptedCellFactoryProtocol { var cellTypes: [AdaptedCellProtocol.Type] { get } func generateCell(viewModel: AdaptedCellViewModelProtocol, tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell }
Добавим поле cellFactory и в didSet поместим регистрацию всех ячеек.
class AdaptedTableView: UITableView { // MARK: - Public properties var viewModel: AdaptedViewModelInputProtocol? var cellFactory: AdaptedCellFactoryProtocol? { didSet { cellFactory?.cellTypes.forEach({ $0.register(self)}) } } ... }
Исправим метод делегата.
extension AdaptedTableView: UITableViewDataSource { ... func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cellFactory = cellFactory, let cellViewModel = viewModel?.sections[indexPath.section].cells[indexPath.row] else { return UITableViewCell() } return cellFactory.generateCell(viewModel: cellViewModel, tableView: tableView, for: indexPath) } }
Использование
С необходимы абстракциями на этом все, пора перейти к конкретным реализациям.
1. Ячейка
В качестве примера я создам ячейку с лейблом по центру и viewModel к ней. Реализация ячейки с кнопкой и картинкой.
protocol TextCellViewModelInputProtocol { var text: String { get } } typealias TextCellViewModelType = AdaptedCellViewModelProtocol & TextCellViewModelInputProtocol class TextCellViewModel: TextCellViewModelType { var text: String init(text: String) { self.text = text } } final class TextTableViewCell: UITableViewCell, AdaptedCellProtocol { // MARK: - IBOutlets @IBOutlet private weak var label: UILabel! // MARK: - Public properties var viewModel: TextCellViewModelInputProtocol? { didSet { bindViewModel() } } // MARK: - Private methods private func bindViewModel() { label.text = viewModel?.text } }
2. Cекция
class AdaptedSectionViewModel: AdaptedSectionViewModelProtocol { // MARK: - Public properties var cells: [AdaptedCellViewModelProtocol] // MARK: - Init init(cells: [AdaptedCellViewModelProtocol]) { self.cells = cells } }
3. Фабрика
struct MainCellFactory: AdaptedSectionFactoryProtocol { var cellTypes: [AdaptedCellProtocol.Type] = [ TextTableViewCell.self ] func generateCell(viewModel: AdaptedCellViewModelProtocol, tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell { switch viewModel { case let viewModel as TextCellViewModelType: let view = TextTableViewCell.reuse(tableView, for: indexPath) view.viewModel = viewModel return view default: return UITableViewCell() } } }
Ну и напоследок нам понадобится viewModel самого модуля.
final class MainViewModel: AdaptedSectionViewModelType { // MARK: - Public properties var sections: [AdaptedSectionViewModelProtocol] // MARK: - Init init() { self.sections = [] self.setupMainSection() } // MARK: - Private methods private func setupMainSection() { let section = AdaptedSectionViewModel(cells: [ TextCellViewModel(text: "Hello!"), TextCellViewModel(text: "It's UITableView with using MVVM") ]) sections.append(section) } }
Все готово, пора добавить UITableView на ViewController, установив в качестве custom class наш AdaptedTableView.

В реальном проекте, MVVM очень часто используют с каким-то паттерном навигации, это может быть координатор или роутер. В зону ответственности таких объектов входит DI (Dependency Injection) внедрение всех необходимых модулю зависимостей. Так как это тестовый проект, я захардкодил viewModel и cellFactory прямо во ViewController.
class ViewController: UIViewController { // MARK: - IBOutlets @IBOutlet weak var tableView: AdaptedTableView! { didSet { tableView.viewModel = MainViewModel() tableView.cellFactory = MainCellFactory() tableView.setup() } } }
Результат
Вывод
В итоге мы получили решение, которое позволяет удобно использовать UITableView с MVVM. Стало очень просто работать с секциями, настраивать ячейки, писать меньше шаблонного кода. В то же время осталась возможность настройки таблицы и расширения функционала при необходимости.
Весь код представленный в этой статье можно скачать по этой ссылке.
ссылка на оригинал статьи https://habr.com/ru/post/531446/
Добавить комментарий